Statistics
| Branch: | Tag: | Revision:

root / devflow / autopkg.py @ 0dcfcb5f

History | View | Annotate | Download (11.9 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
from configobj import ConfigObj
41

    
42
from devflow import versioning
43

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

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

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

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

    
63

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

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

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

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

83
"""
84

    
85

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

    
89

    
90
def main():
91
    from devflow.version import __version__
92
    parser = OptionParser(usage="usage: %prog [options] mode",
93
                          version="devflow %s" % __version__,
94
                          add_help_option=False)
95
    parser.add_option("-h", "--help",
96
                      action="store_true",
97
                      default=False,
98
                      help="show this help message")
99
    parser.add_option("-k", "--keep-repo",
100
                      action="store_true",
101
                      dest="keep_repo",
102
                      default=False,
103
                      help="Do not delete the cloned repository")
104
    parser.add_option("-b", "--build-dir",
105
                      dest="build_dir",
106
                      default=None,
107
                      help="Directory to store created pacakges")
108
    parser.add_option("-r", "--repo-dir",
109
                      dest="repo_dir",
110
                      default=None,
111
                      help="Directory to clone repository")
112
    parser.add_option("-d", "--dirty",
113
                      dest="force_dirty",
114
                      default=False,
115
                      action="store_true",
116
                      help="Do not check if working directory is dirty")
117
    parser.add_option("-c", "--config-file",
118
                      dest="config_file",
119
                      help="Override default configuration file")
120
    parser.add_option("--no-sign",
121
                      dest="sign",
122
                      action="store_false",
123
                      default=True,
124
                      help="Do not sign the packages")
125
    parser.add_option("--key-id",
126
                      dest="keyid",
127
                      help="Use this keyid for gpg signing")
128
    parser.add_option("--dist",
129
                      dest="dist",
130
                      default="unstable",
131
                      help="If running in snapshot mode, automatically set"
132
                            " the changelog distribution to this value"
133
                            " (default=unstable).")
134

    
135
    (options, args) = parser.parse_args()
136

    
137
    if options.help:
138
        print_help(parser.get_prog_name())
139
        parser.print_help()
140
        return
141

    
142
    # Get build mode
143
    try:
144
        mode = args[0]
145
    except IndexError:
146
        raise ValueError("Mode argument is mandatory. Usage: %s"
147
                         % parser.usage)
148
    if mode not in AVAILABLE_MODES:
149
        raise ValueError(red("Invalid argument! Mode must be one: %s"
150
                         % ", ".join(AVAILABLE_MODES)))
151

    
152
    os.environ["DEVFLOW_BUILD_MODE"] = mode
153

    
154
    # Load the repository
155
    try:
156
        original_repo = git.Repo(".")
157
    except git.git.InvalidGitRepositoryError:
158
        raise RuntimeError(red("Current directory is not git repository."))
159

    
160
    # Check that repository is clean
161
    toplevel = original_repo.working_dir
162
    if original_repo.is_dirty() and not options.force_dirty:
163
        raise RuntimeError(red("Repository %s is dirty." % toplevel))
164

    
165
    # Get packages from configuration file
166
    config_file = options.config_file or os.path.join(toplevel, "devflow.conf")
167
    config = ConfigObj(config_file)
168
    packages = config['packages'].keys()
169
    print_green("Will build the following packages:\n" + "\n".join(packages))
170

    
171
    # Get current branch name and type and check if it is a valid one
172
    branch = original_repo.head.reference.name
173
    branch_type = versioning.get_branch_type(branch)
174

    
175
    if branch_type not in BRANCH_TYPES.keys():
176
        allowed_branches = ", ".join(BRANCH_TYPES.keys())
177
        raise ValueError("Malformed branch name '%s', cannot classify as"
178
                         " one of %s" % (branch, allowed_branches))
179

    
180
    # Check that original repo has the correct debian branch
181
    debian_branch = "debian-" + branch
182
    origin_debian = "origin/" + debian_branch
183
    if not debian_branch in original_repo.branches:
184
        # Get default debian branch
185
        default_debian = BRANCH_TYPES[branch_type].default_debian_branch
186
        origin_debian = "origin/" + default_debian
187
        if not default_debian in original_repo.branches:
188
            original_repo.git.branch(default_debian,
189
                                     origin_debian)
190

    
191
    # Clone the repo
192
    repo_dir = options.repo_dir or create_temp_directory("df-repo")
193
    repo = original_repo.clone(repo_dir)
194
    print_green("Cloned repository to '%s'." % repo_dir)
195

    
196
    # Create the debian branch
197
    repo.git.branch(debian_branch, origin_debian)
198
    print_green("Created branch '%s' to track '%s'" % (debian_branch,
199
                origin_debian))
200

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

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

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

    
216
    # Update the version files
217
    versioning.update_version()
218

    
219
    # Tag branch with python version
220
    branch_tag = python_version
221
    repo.git.tag(branch_tag, branch)
222
    upstream_tag = "upstream/" + branch_tag
223
    repo.git.tag(upstream_tag, branch)
224

    
225
    # Update changelog
226
    dch = git_dch("--debian-branch=%s" % debian_branch,
227
                  "--git-author",
228
                  "--ignore-regex=\".*\"",
229
                  "--multimaint-merge",
230
                  "--since=HEAD",
231
                  "--new-version=%s" % debian_version)
232
    print_green("Successfully ran '%s'" % " ".join(dch.cmd))
233

    
234
    if mode == "release":
235
        call("vim debian/changelog")
236
    else:
237
        f = open("debian/changelog", 'r+')
238
        lines = f.readlines()
239
        lines[0] = lines[0].replace("UNRELEASED", options.dist)
240
        lines[2] = lines[2].replace("UNRELEASED", "Snapshot build")
241
        f.seek(0)
242
        f.writelines(lines)
243
        f.close()
244

    
245
    # Add changelog to INDEX
246
    repo.git.add("debian/changelog")
247
    # Commit Changes
248
    repo.git.commit("-s", "-a", m="Bump version to %s" % debian_version)
249
    # Tag debian branch
250
    debian_branch_tag = "debian/" + branch_tag
251
    repo.git.tag(debian_branch_tag)
252

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

    
256
    # Create debian packages
257
    build_dir = options.build_dir or create_temp_directory("df-build")
258
    print_green("Build directory: '%s'" % build_dir)
259

    
260
    cd(repo_dir)
261
    version_files = []
262
    for _, pkg_info in config['packages'].items():
263
        version_files.append(pkg_info['version_file'])
264
    ignore_regexp = "|".join(["^(%s)$" % vf for vf in version_files])
265
    build_cmd = "git-buildpackage --git-export-dir=%s"\
266
                " --git-upstream-branch=%s --git-debian-branch=%s"\
267
                " --git-export=INDEX --git-ignore-new -sa"\
268
                " --source-option='\"--extend-diff-ignore=%s\"'"\
269
                " --git-upstream-tag=%s"\
270
                % (build_dir, branch, debian_branch, ignore_regexp,
271
                   upstream_tag)
272
    if not options.sign:
273
        build_cmd += " -uc -us"
274
    elif options.keyid:
275
        build_cmd += " -k\"'%s'\"" % options.keyid
276
    call(build_cmd)
277

    
278
    # Remove cloned repo
279
    if mode != 'release' and not options.keep_repo:
280
        print_green("Removing cloned repo '%s'." % repo_dir)
281
        rm("-r", repo_dir)
282

    
283
    # Print final info
284
    info = (("Version", debian_version),
285
            ("Upstream branch", branch),
286
            ("Upstream tag", branch_tag),
287
            ("Debian branch", debian_branch),
288
            ("Debian tag", debian_branch_tag),
289
            ("Repository directory", repo_dir),
290
            ("Packages directory", build_dir))
291
    print_green("\n".join(["%s: %s" % (name, val) for name, val in info]))
292

    
293
    # Print help message
294
    if mode == "release":
295
        origin = original_repo.remote().url
296
        repo.create_remote("original_origin", origin)
297
        print_green("Created remote 'original_origin' for the repository '%s'"
298
                    % origin)
299

    
300
        print_green("To update repositories '%s' and '%s' go to '%s' and run:"
301
                    % (toplevel, origin, repo_dir))
302
        for remote in ['origin', 'original_origin']:
303
            print
304
            for obj in [debian_branch, branch_tag, debian_branch_tag]:
305
                print_green("git push %s %s" % (remote, obj))
306

    
307

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

    
312

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

    
318

    
319
def create_temp_directory(suffix):
320
    create_dir_cmd = mktemp("-d", "/tmp/" + suffix + "-XXXXX")
321
    return create_dir_cmd.stdout.strip()
322

    
323

    
324
if __name__ == "__main__":
325
    sys.exit(main())