Statistics
| Branch: | Tag: | Revision:

root / devflow / autopkg.py @ 06dcdc1b

History | View | Annotate | Download (11.6 kB)

1
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import git
35
import os
36
import sys
37
from optparse import OptionParser
38
from collections import namedtuple
39
from sh import mktemp, cd, rm, git_dch, python
40

    
41
from devflow import versioning
42

    
43
try:
44
    from colors import red, green
45
except ImportError:
46
    red = lambda x: x
47
    green = lambda x: x
48

    
49
print_red = lambda x: sys.stdout.write(red(x) + "\n")
50
print_green = lambda x: sys.stdout.write(green(x) + "\n")
51

    
52
AVAILABLE_MODES = ["release", "snapshot"]
53

    
54
branch_type = namedtuple("branch_type", ["default_debian_branch"])
55
BRANCH_TYPES = {
56
    "feature": branch_type("debian-develop"),
57
    "develop": branch_type("debian-develop"),
58
    "release": branch_type("debian-develop"),
59
    "master": branch_type("debian"),
60
    "hotfix": branch_type("debian")}
61

    
62

    
63
DESCRIPTION = """Tool for automatical build of debian packages.
64

65
%(prog)s is a helper script for automatic build of debian packages from
66
repositories that follow the `git flow` development model
67
<http://nvie.com/posts/a-successful-git-branching-model/>.
68

69
This script must run from inside a clean git repository and will perform the
70
following steps:
71
    * Clone your repository to a temporary directory
72
    * Merge the current branch with the corresponding debian branch
73
    * Compute the version of the new package and update the python
74
      version files
75
    * Create a new entry in debian/changelog, using `git-dch`
76
    * Create the debian packages, using `git-buildpackage`
77
    * Tag the appropriate branches if in `release` mode
78

79
%(prog)s will work with the packages that are declared in `autopkg.conf`
80
file, which must exist in the toplevel directory of the git repository.
81

82
"""
83

    
84

    
85
def print_help(prog):
86
    print DESCRIPTION % {"prog": prog}
87

    
88

    
89
def main():
90
    from devflow.version import __version__
91
    parser = OptionParser(usage="usage: %prog [options] mode",
92
                          version="devflow %s" % __version__,
93
                          add_help_option=False)
94
    parser.add_option("-h", "--help",
95
                      action="store_true",
96
                      default=False,
97
                      help="show this help message")
98
    parser.add_option("-k", "--keep-repo",
99
                      action="store_true",
100
                      dest="keep_repo",
101
                      default=False,
102
                      help="Do not delete the cloned repository")
103
    parser.add_option("-b", "--build-dir",
104
                      dest="build_dir",
105
                      default=None,
106
                      help="Directory to store created pacakges")
107
    parser.add_option("-r", "--repo-dir",
108
                      dest="repo_dir",
109
                      default=None,
110
                      help="Directory to clone repository")
111
    parser.add_option("-d", "--dirty",
112
                      dest="force_dirty",
113
                      default=False,
114
                      action="store_true",
115
                      help="Do not check if working directory is dirty")
116
    parser.add_option("-c", "--config-file",
117
                      dest="config_file",
118
                      help="Override default configuration file")
119

    
120
    (options, args) = parser.parse_args()
121

    
122
    if options.help:
123
        print_help(parser.get_prog_name())
124
        parser.print_help()
125
        return
126

    
127
    # Get build mode
128
    try:
129
        mode = args[0]
130
    except IndexError:
131
        raise ValueError("Mode argument is mandatory. Usage: %s"
132
                         % parser.usage)
133
    if mode not in AVAILABLE_MODES:
134
        raise ValueError(red("Invalid argument! Mode must be one: %s"
135
                         % ", ".join(AVAILABLE_MODES)))
136

    
137
    os.environ["GITFLOW_BUILD_MODE"] = mode
138

    
139
    # Load the repository
140
    try:
141
        original_repo = git.Repo(".")
142
    except git.git.InvalidGitRepositoryError:
143
        raise RuntimeError(red("Current directory is not git repository."))
144

    
145
    # Check that repository is clean
146
    toplevel = original_repo.working_dir
147
    if original_repo.is_dirty() and not options.force_dirty:
148
        raise RuntimeError(red("Repository %s is dirty." % toplevel))
149

    
150
    # Get packages from configuration file
151
    config_file = options.config_file or os.path.join(toplevel, "autopkg.conf")
152
    packages = get_packages_to_build(config_file)
153
    if packages:
154
        print_green("Will build the following packages:\n"
155
                    "\n".join(packages))
156
    else:
157
        raise RuntimeError("Configuration file is empty."
158
                           " No packages to build.")
159

    
160
    # Clone the repo
161
    repo_dir = options.repo_dir
162
    if not repo_dir:
163
        repo_dir = create_temp_directory("df-repo")
164
        print_green("Created temporary directory '%s' for the cloned repo."
165
                    % repo_dir)
166

    
167
    repo = original_repo.clone(repo_dir)
168
    print_green("Cloned current repository to '%s'." % repo_dir)
169

    
170
    reflog_hexsha = repo.head.log()[-1].newhexsha
171
    print "Latest Reflog entry is %s" % reflog_hexsha
172

    
173
    branch = repo.head.reference.name
174
    allowed_branches = ", ".join(x for x in BRANCH_TYPES.keys())
175
    if branch.split('-')[0] not in allowed_branches:
176
        raise ValueError("Malformed branch name '%s', cannot classify as"
177
                         " one of %s" % (branch, allowed_branches))
178

    
179
    brnorm = versioning.normalize_branch_name(branch)
180
    btypestr = versioning.get_branch_type(brnorm)
181

    
182
    # Find the debian branch, and create it if does not exist
183
    debian_branch = "debian-" + brnorm
184
    origin_debian = "origin/" + debian_branch
185
    if not origin_debian in repo.references:
186
        # Get default debian branch
187
        try:
188
            default_debian = BRANCH_TYPES[btypestr].default_debian_branch
189
            origin_debian = "origin/" + default_debian
190
        except KeyError:
191
            allowed_branches = ", ".join(x for x in BRANCH_TYPES.keys())
192
            raise ValueError("Malformed branch name '%s', cannot classify as"
193
                             " one of %s" % (btypestr, allowed_branches))
194

    
195
    repo.git.branch("--track", debian_branch, origin_debian)
196
    print_green("Created branch '%s' to track '%s'" % (debian_branch,
197
                origin_debian))
198

    
199
    # Go to debian branch
200
    repo.git.checkout(debian_branch)
201
    print_green("Changed to branch '%s'" % debian_branch)
202

    
203
    # Merge with starting branch
204
    repo.git.merge(branch)
205
    print_green("Merged branch '%s' into '%s'" % (brnorm, debian_branch))
206

    
207
    # Compute python and debian version
208
    cd(repo_dir)
209
    python_version = versioning.get_python_version()
210
    debian_version = versioning.\
211
        debian_version_from_python_version(python_version)
212
    print_green("The new debian version will be: '%s'" % debian_version)
213

    
214
    # Update changelog
215
    dch = git_dch("--debian-branch=%s" % debian_branch,
216
                  "--git-author",
217
                  "--ignore-regex=\".*\"",
218
                  "--multimaint-merge",
219
                  "--since=HEAD",
220
                  "--new-version=%s" % debian_version)
221
    print_green("Successfully ran '%s'" % " ".join(dch.cmd))
222

    
223
    if mode == "release":
224
        # Commit changelog and update tag branches
225
        call("vim debian/changelog")
226
        repo.git.add("debian/changelog")
227
        repo.git.commit("-s", "-a", m="Bump new upstream version")
228
        python_tag = python_version
229
        debian_tag = "debian/" + python_tag
230
        repo.git.tag(debian_tag)
231
        repo.git.tag(python_tag, brnorm)
232
    else:
233
        f = open("debian/changelog", 'r+')
234
        lines = f.readlines()
235
        lines[0] = lines[0].replace("UNRELEASED", "unstable")
236
        lines[2] = lines[2].replace("UNRELEASED", "Snapshot version")
237
        f.seek(0)
238
        f.writelines(lines)
239
        f.close()
240
        repo.git.add("debian/changelog")
241

    
242
    # Update the python version files
243
    # TODO: remove this
244
    for package in packages:
245
        # python setup.py should run in its directory
246
        cd(package)
247
        package_dir = repo_dir + "/" + package
248
        res = python(package_dir + "/setup.py", "sdist", _out=sys.stdout)
249
        print res.stdout
250
        if package != ".":
251
            cd("../")
252

    
253
    # Add version.py files to repo
254
    call("grep \"__version_vcs\" -r . -l -I | xargs git add -f")
255

    
256
    # Create debian branches
257
    build_dir = options.build_dir
258
    if not options.build_dir:
259
        build_dir = create_temp_directory("df-build")
260
        print_green("Created directory '%s' to store the .deb files." %
261
                    build_dir)
262

    
263
    cd(repo_dir)
264
    call("git-buildpackage --git-export-dir=%s --git-upstream-branch=%s"
265
         " --git-debian-branch=%s --git-export=INDEX --git-ignore-new -sa"
266
         % (build_dir, brnorm, debian_branch))
267

    
268
    # Remove cloned repo
269
    if mode != 'release' and not options.keep_repo:
270
        print_green("Removing cloned repo '%s'." % repo_dir)
271
        rm("-r", repo_dir)
272
    else:
273
        print_green("Repository dir '%s'" % repo_dir)
274

    
275
    print_green("Completed. Version '%s', build area: '%s'"
276
                % (debian_version, build_dir))
277

    
278
    # Print help message
279
    if mode == "release":
280
        TAG_MSG = "Tagged branch %s with tag %s\n"
281
        print_green(TAG_MSG % (brnorm, python_tag))
282
        print_green(TAG_MSG % (debian_branch, debian_tag))
283

    
284
        UPDATE_MSG = "To update repository %s, go to %s, and run the"\
285
                     " following commands:\n" + "git push origin %s\n" * 3
286

    
287
        origin_url = repo.remotes['origin'].url
288
        remote_url = original_repo.remotes['origin'].url
289

    
290
        print_green(UPDATE_MSG % (origin_url, repo_dir, debian_branch,
291
                    debian_tag, python_tag))
292
        print_green(UPDATE_MSG % (remote_url, original_repo.working_dir,
293
                    debian_branch, debian_tag, python_tag))
294

    
295

    
296
def get_packages_to_build(config_file):
297
    config_file = os.path.abspath(config_file)
298
    try:
299
        f = open(config_file)
300
    except IOError as e:
301
        raise IOError("Can not access configuration file %s: %s"
302
                      % (config_file, e.strerror))
303

    
304
    lines = [l.strip() for l in f.readlines()]
305
    l = [l for l in lines if not l.startswith("#")]
306
    f.close()
307
    return l
308

    
309

    
310
def create_temp_directory(suffix):
311
    create_dir_cmd = mktemp("-d", "/tmp/" + suffix + "-XXXXX")
312
    return create_dir_cmd.stdout.strip()
313

    
314

    
315
def call(cmd):
316
    rc = os.system(cmd)
317
    if rc:
318
        raise RuntimeError("Command '%s' failed!" % cmd)
319

    
320
if __name__ == "__main__":
321
    sys.exit(main())