Statistics
| Branch: | Tag: | Revision:

root / devflow / autopkg.py @ d3af231f

History | View | Annotate | Download (13 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
"""Helper script for automatic build of debian packages."""
35

    
36
import os
37
import sys
38

    
39
from git import GitCommandError
40
from optparse import OptionParser
41
from sh import mktemp, cd, rm, git_dch  # pylint: disable=E0611
42

    
43
from devflow import versioning
44
from devflow import utils
45
from devflow import BRANCH_TYPES
46

    
47
if sys.stdout.isatty():
48
    try:
49
        import colors
50
        use_colors = True
51
    except AttributeError:
52
        use_colors = False
53
else:
54
    use_colors = False
55

    
56

    
57
if use_colors:
58
    red = colors.red
59
    green = colors.green
60
else:
61
    red = lambda x: x
62
    green = lambda x: x
63

    
64
print_red = lambda x: sys.stdout.write(red(x) + "\n")
65
print_green = lambda x: sys.stdout.write(green(x) + "\n")
66

    
67
AVAILABLE_MODES = ["release", "snapshot"]
68

    
69
DESCRIPTION = """Tool for automatical build of debian packages.
70

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

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

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

88
"""
89

    
90

    
91
def print_help(prog):
92
    print DESCRIPTION % {"prog": prog}
93

    
94

    
95
def main():
96
    from devflow.version import __version__  # pylint: disable=E0611,F0401
97
    parser = OptionParser(usage="usage: %prog [options] mode",
98
                          version="devflow %s" % __version__,
99
                          add_help_option=False)
100
    parser.add_option("-h", "--help",
101
                      action="store_true",
102
                      default=False,
103
                      help="show this help message")
104
    parser.add_option("-k", "--keep-repo",
105
                      action="store_true",
106
                      dest="keep_repo",
107
                      default=False,
108
                      help="Do not delete the cloned repository")
109
    parser.add_option("-b", "--build-dir",
110
                      dest="build_dir",
111
                      default=None,
112
                      help="Directory to store created pacakges")
113
    parser.add_option("-r", "--repo-dir",
114
                      dest="repo_dir",
115
                      default=None,
116
                      help="Directory to clone repository")
117
    parser.add_option("-d", "--dirty",
118
                      dest="force_dirty",
119
                      default=False,
120
                      action="store_true",
121
                      help="Do not check if working directory is dirty")
122
    parser.add_option("-c", "--config-file",
123
                      dest="config_file",
124
                      help="Override default configuration file")
125
    parser.add_option("--no-sign",
126
                      dest="sign",
127
                      action="store_false",
128
                      default=True,
129
                      help="Do not sign the packages")
130
    parser.add_option("--key-id",
131
                      dest="keyid",
132
                      help="Use this keyid for gpg signing")
133
    parser.add_option("--dist",
134
                      dest="dist",
135
                      default=None,
136
                      help="Force distribution in Debian changelog"),
137
    parser.add_option("-S", "--source-only",
138
                      dest="source_only",
139
                      default=False,
140
                      action="store_true",
141
                      help="Specifies a source-only build, no binary packages"
142
                           " need to be made.")
143
    parser.add_option("--debian-branch",
144
                      dest="debian_branch",
145
                      default=None,
146
                      help="Use this debian branch, instead of"
147
                           "auto-discovering the debian branch to use")
148

    
149
    (options, args) = parser.parse_args()
150

    
151
    if options.help:
152
        print_help(parser.get_prog_name())
153
        parser.print_help()
154
        return
155

    
156
    # Get build mode
157
    try:
158
        mode = args[0]
159
    except IndexError:
160
        mode = utils.get_build_mode()
161
    if mode not in AVAILABLE_MODES:
162
        raise ValueError(red("Invalid argument! Mode must be one: %s"
163
                         % ", ".join(AVAILABLE_MODES)))
164

    
165
    # Load the repository
166
    original_repo = utils.get_repository()
167

    
168
    # Check that repository is clean
169
    toplevel = original_repo.working_dir
170
    if original_repo.is_dirty() and not options.force_dirty:
171
        raise RuntimeError(red("Repository %s is dirty." % toplevel))
172

    
173
    # Get packages from configuration file
174
    config = utils.get_config(options.config_file)
175
    packages = config['packages'].keys()
176
    print_green("Will build the following packages:\n" + "\n".join(packages))
177

    
178
    # Get current branch name and type and check if it is a valid one
179
    branch = original_repo.head.reference.name
180
    branch = utils.undebianize(branch)
181
    branch_type_str = utils.get_branch_type(branch)
182

    
183
    if branch_type_str not in BRANCH_TYPES.keys():
184
        allowed_branches = ", ".join(BRANCH_TYPES.keys())
185
        raise ValueError("Malformed branch name '%s', cannot classify as"
186
                         " one of %s" % (branch, allowed_branches))
187

    
188
    # Fix needed environment variables
189
    os.environ["DEVFLOW_BUILD_MODE"] = mode
190
    git_config = original_repo.config_reader()
191
    try:
192
        os.environ["DEBFULLNAME"] = git_config.get_value("user", "name")
193
        os.environ["DEBEMAIL"] = git_config.get_value("user", "email")
194
    except:
195
        print "Could not load user/email from config"
196

    
197
    # Check that base version file and branch are correct
198
    versioning.get_python_version()
199

    
200
    # Get the debian branch
201
    if options.debian_branch:
202
        debian_branch = options.debian_branch
203
    else:
204
        debian_branch = utils.get_debian_branch(branch)
205
    origin_debian = "origin/" + debian_branch
206

    
207
    # Clone the repo
208
    repo_dir = options.repo_dir or create_temp_directory("df-repo")
209
    repo_dir = os.path.abspath(repo_dir)
210
    repo = original_repo.clone(repo_dir, branch=branch)
211
    print_green("Cloned repository to '%s'." % repo_dir)
212

    
213
    build_dir = options.build_dir or create_temp_directory("df-build")
214
    build_dir = os.path.abspath(build_dir)
215
    print_green("Build directory: '%s'" % build_dir)
216

    
217
    # Create the debian branch
218
    repo.git.branch(debian_branch, origin_debian)
219
    print_green("Created branch '%s' to track '%s'" % (debian_branch,
220
                origin_debian))
221

    
222
    # Go to debian branch
223
    repo.git.checkout(debian_branch)
224
    print_green("Changed to branch '%s'" % debian_branch)
225

    
226
    # Merge with starting branch
227
    repo.git.merge(branch)
228
    print_green("Merged branch '%s' into '%s'" % (branch, debian_branch))
229

    
230
    # Compute python and debian version
231
    cd(repo_dir)
232
    python_version = versioning.get_python_version()
233
    debian_version = versioning.\
234
        debian_version_from_python_version(python_version)
235
    print_green("The new debian version will be: '%s'" % debian_version)
236

    
237
    # Update the version files
238
    versioning.update_version()
239

    
240

    
241
    if not options.sign:
242
        sign_tag_opt = None
243
    elif options.keyid:
244
        sign_tag_opt = "-u=%s" % options.keyid
245
    elif mode == "release":
246
        sign_tag_opt = "-s"
247
    else:
248
        sign_tag_opt = None
249

    
250
    # Tag branch with python version
251
    branch_tag = python_version
252
    tag_message = "%s version %s" % (mode.capitalize(), python_version)
253
    try:
254
        repo.git.tag(branch_tag, branch, sign_tag_opt, "-m=%s" % tag_message)
255
    except GitCommandError:
256
        # Tag may already exist, if only the debian branch has changed
257
        pass
258
    upstream_tag = "upstream/" + branch_tag
259
    repo.git.tag(upstream_tag, branch)
260

    
261
    # Update changelog
262
    dch = git_dch("--debian-branch=%s" % debian_branch,
263
                  "--git-author",
264
                  "--ignore-regex=\".*\"",
265
                  "--multimaint-merge",
266
                  "--since=HEAD",
267
                  "--new-version=%s" % debian_version)
268
    print_green("Successfully ran '%s'" % " ".join(dch.cmd))
269

    
270
    if options.dist is not None:
271
        distribution = options.dist
272
    elif mode == "release":
273
        distribution = utils.get_distribution_codename()
274
    else:
275
        distribution = "unstable"
276

    
277
    f = open("debian/changelog", 'r+')
278
    lines = f.readlines()
279
    lines[0] = lines[0].replace("UNRELEASED", distribution)
280
    lines[2] = lines[2].replace("UNRELEASED", "%s build" % mode)
281
    f.seek(0)
282
    f.writelines(lines)
283
    f.close()
284

    
285
    if mode == "release":
286
        call("vim debian/changelog")
287

    
288
    # Add changelog to INDEX
289
    repo.git.add("debian/changelog")
290
    # Commit Changes
291
    repo.git.commit("-s", "-a", m="Bump version to %s" % debian_version)
292
    # Tag debian branch
293
    debian_branch_tag = "debian/" + utils.version_to_tag(debian_version)
294
    tag_message = "%s version %s" % (mode.capitalize(), debian_version)
295
    if mode == "release":
296
        repo.git.tag(debian_branch_tag, sign_tag_opt, "-m=%s" % tag_message)
297

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

    
301
    # Create debian packages
302
    cd(repo_dir)
303
    version_files = []
304
    for _, pkg_info in config['packages'].items():
305
        version_files.append(pkg_info['version_file'])
306
    ignore_regexp = "|".join(["^(%s)$" % vf for vf in version_files])
307
    build_cmd = "git-buildpackage --git-export-dir=%s"\
308
                " --git-upstream-branch=%s --git-debian-branch=%s"\
309
                " --git-export=INDEX --git-ignore-new -sa"\
310
                " --source-option='\"--extend-diff-ignore=%s\"'"\
311
                " --git-upstream-tag=%s"\
312
                % (build_dir, branch, debian_branch, ignore_regexp,
313
                   upstream_tag)
314
    if options.source_only:
315
        build_cmd += " -S"
316
    if not options.sign:
317
        build_cmd += " -uc -us"
318
    elif options.keyid:
319
        build_cmd += " -k\"'%s'\"" % options.keyid
320
    call(build_cmd)
321

    
322
    # Remove cloned repo
323
    if mode != 'release' and not options.keep_repo:
324
        print_green("Removing cloned repo '%s'." % repo_dir)
325
        rm("-r", repo_dir)
326

    
327
    # Print final info
328
    info = (("Version", debian_version),
329
            ("Upstream branch", branch),
330
            ("Upstream tag", branch_tag),
331
            ("Debian branch", debian_branch),
332
            ("Debian tag", debian_branch_tag),
333
            ("Repository directory", repo_dir),
334
            ("Packages directory", build_dir))
335
    print_green("\n".join(["%s: %s" % (name, val) for name, val in info]))
336

    
337
    # Print help message
338
    if mode == "release":
339
        origin = original_repo.remote().url
340
        repo.create_remote("original_origin", origin)
341
        print_green("Created remote 'original_origin' for the repository '%s'"
342
                    % origin)
343

    
344
        print_green("To update repositories '%s' and '%s' go to '%s' and run:"
345
                    % (toplevel, origin, repo_dir))
346
        for remote in ['origin', 'original_origin']:
347
            objects = [debian_branch, branch_tag, debian_branch_tag]
348
            print_green("git push %s %s" % (remote, " ".join(objects)))
349

    
350

    
351
def create_temp_directory(suffix):
352
    create_dir_cmd = mktemp("-d", "/tmp/" + suffix + "-XXXXX")
353
    return create_dir_cmd.stdout.strip()
354

    
355

    
356
def call(cmd):
357
    rc = os.system(cmd)
358
    if rc:
359
        raise RuntimeError("Command '%s' failed!" % cmd)
360

    
361

    
362
if __name__ == "__main__":
363
    sys.exit(main())