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