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()) |