backportpackage: new script for testing backport requests in a PPA.

This commit is contained in:
Benjamin Drung 2010-12-16 23:43:48 +01:00
commit 8eda0c4326
8 changed files with 596 additions and 147 deletions

280
backportpackage Executable file
View File

@ -0,0 +1,280 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# ##################################################################
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# See file /usr/share/common-licenses/GPL-2 for more details.
#
# ##################################################################
import optparse
import os
import shutil
import subprocess
import sys
import tempfile
import urllib
from debian.deb822 import Dsc
import launchpadlib.launchpad
import lsb_release
from ubuntutools.builder import getBuilder
from ubuntutools.logger import Logger
def error(msg):
Logger.error(msg)
sys.exit(1)
def check_call(cmd, *args, **kwargs):
Logger.command(cmd)
ret = subprocess.call(cmd, *args, **kwargs)
if ret != 0:
error('%s returned %d' % (cmd, ret))
def parse(args):
usage = 'Usage: %prog [options] <source package name or .dsc URL/file>'
p = optparse.OptionParser(usage)
p.add_option('-d', '--destination',
dest='dest_releases',
default=[],
action='append',
help='Backport to DEST release (default: current release)',
metavar='DEST')
p.add_option('-s', '--source',
dest='source_release',
default=None,
help='Backport from SOURCE release (default: devel release)',
metavar='SOURCE')
p.add_option('-b', '--build',
dest='build',
default=False,
action='store_true',
help='Build the package before uploading (default: %default)')
p.add_option('-B', '--builder',
dest='builder',
default=None,
help='Specify the package builder (default: pbuilder)',
metavar='BUILDER')
p.add_option('-u', '--upload',
dest='upload',
help='Specify an upload destination',
metavar='UPLOAD')
p.add_option('-v', '--version',
dest='version',
default=None,
help='Package version to backport (or verify)',
metavar='VERSION')
p.add_option('-w', '--workdir',
dest='workdir',
default=None,
help='Specify a working directory (default: temporary dir)',
metavar='WORKDIR')
p.add_option('-l', '--launchpad',
dest='launchpad',
default='production',
help='Launchpad instance to connect to (default: %default)',
metavar='INSTANCE')
opts, args = p.parse_args(args)
if len(args) != 1:
p.error('You must specify a single source package or a .dsc URL/path')
if not opts.upload and not opts.build:
p.error('Nothing to do')
return opts, args
def find_release_package(lp, package, version, source_release):
ubuntu = lp.distributions['ubuntu']
archive = ubuntu.main_archive
series = ubuntu.getSeries(name_or_version=source_release)
status = 'Published'
for pocket in ('Updates', 'Security', 'Release'):
try:
srcpkg = archive.getPublishedSources(source_name=package,
distro_series=series,
pocket=pocket,
status=status,
exact_match=True)[0]
break
except IndexError:
continue
else:
error('Unable to find package %s in release %s' %
(package, source_release))
if version and version != srcpkg.source_package_version:
error('Requested backport of version %s but %s is at version %s' %
(version, package, srcpkg.source_package_version))
return srcpkg
def find_version_package(lp, package, version):
ubuntu = lp.distributions['ubuntu']
archive = ubuntu.main_archive
try:
# Might get more than one (i.e. same version in multiple
# releases), but they should all be identical
return archive.getPublishedSources(source_name=package,
version=version)[0]
except IndexError:
error('Version %s of package %s was never published in Ubuntu' %
(version, package))
def dscurl_from_package(lp, workdir, package, version, source_release):
if not source_release and not version:
source_release = lp.distributions['ubuntu'].current_series.name
# If source_release is specified, then version is just for verification
if source_release:
srcpkg = find_release_package(lp, package, version, source_release)
else:
srcpkg = find_version_package(lp, package, version)
for f in srcpkg.sourceFileUrls():
if f.endswith('.dsc'):
return urllib.unquote(f)
else:
error('Package %s contains no .dsc file' % package)
def dscurl_from_dsc(package):
path = os.path.abspath(os.path.expanduser(package))
if os.path.exists(path):
return 'file://%s' % path
else:
# Can't resolve it as a local path? Let's just hope it's good as-is
return package
def fetch_package(lp, workdir, package, version, source_release):
# Returns the path to the .dsc file that was fetched
if package.endswith('.dsc'):
dsc = dscurl_from_dsc(package)
else:
dsc = dscurl_from_package(lp, workdir, package, version, source_release)
check_call(['dget', '--download-only', '--allow-unauthenticated', dsc],
cwd=workdir)
return os.path.join(workdir, os.path.basename(dsc))
def get_backport_version(version, upload, release):
v = version + ('~%s1' % release)
if upload and upload.startswith('ppa:'):
v += '~ppa1'
return v
def get_backport_dist(upload, release):
if not upload or upload == 'ubuntu':
return '%s-backports' % release
else:
return release
def do_build(workdir, package, release, bp_version, builder):
builder = getBuilder(builder)
if not builder:
return
return builder.build(os.path.join(workdir,
'%s_%s.dsc' % (package, bp_version)),
release,
workdir)
def do_upload(workdir, package, bp_version, upload):
prompt = 'Do you want to upload this to %s? [Y/n]' % upload
while True:
answer = raw_input(prompt).strip().lower()
if answer in ('', 'y', 'yes'):
break
elif answer in ('n', 'no'):
return
check_call(['dput', upload, '%s_%s_source.changes' % (package, bp_version)],
cwd=workdir)
def do_backport(workdir, package, dscfile, version, release, build, builder, upload):
check_call(['dpkg-source', '-x', dscfile, package], cwd=workdir)
srcdir = os.path.join(workdir, package)
bp_version = get_backport_version(version, upload, release)
bp_dist = get_backport_dist(upload, release)
check_call(['dch',
'--force-bad-version',
'--preserve',
'--newversion', bp_version,
'--distribution', bp_dist,
'No-change backport to %s' % release],
cwd=srcdir)
check_call(['debuild', '-S', '-sa'], cwd=srcdir)
if ':' in bp_version:
bp_version = bp_version[bp_version.find(':')+1:]
print 'Please check the package in file://%s carefully' % workdir
if build:
if 0 != do_build(workdir, package, release, bp_version, builder):
error('Package failed to build; aborting')
if upload:
do_upload(workdir, package, bp_version, upload)
shutil.rmtree(srcdir)
def main(args):
os.environ['DEB_VENDOR'] = 'Ubuntu'
opts, (package_or_dsc,) = parse(args[1:])
script_name = os.path.basename(sys.argv[0])
lp = launchpadlib.launchpad.Launchpad.login_anonymously(script_name,
opts.launchpad)
if not opts.dest_releases:
try:
distinfo = lsb_release.get_distro_information()
opts.dest_releases = [distinfo['CODENAME']]
except:
error('No destination release specified and unable to guess yours')
if opts.workdir:
workdir = os.path.expanduser(opts.workdir)
else:
workdir = tempfile.mkdtemp(prefix='backportpackage-')
if not os.path.exists(workdir):
os.makedirs(workdir)
try:
dscfile = fetch_package(lp,
workdir,
package_or_dsc,
opts.version,
opts.source_release)
dsc = Dsc(open(os.path.join(workdir, dscfile)))
package = dsc['Source']
version = dsc['Version']
for release in opts.dest_releases:
do_backport(workdir,
package,
dscfile,
version,
release,
opts.build,
opts.builder,
opts.upload)
finally:
if not opts.workdir:
shutil.rmtree(workdir)
if __name__ == '__main__':
sys.exit(main(sys.argv))

5
debian/changelog vendored
View File

@ -12,7 +12,10 @@ ubuntu-dev-tools (0.108) UNRELEASED; urgency=low
[ Colin Watson ]
* grep-merges: New tool.
-- Benjamin Drung <bdrung@ubuntu.com> Tue, 14 Dec 2010 18:21:37 +0100
[ Evan Broder ]
* backportpackage: new script for testing backport requests in a PPA.
-- Benjamin Drung <bdrung@ubuntu.com> Thu, 16 Dec 2010 23:40:14 +0100
ubuntu-dev-tools (0.107) experimental; urgency=low

105
doc/backportpackage.1 Normal file
View File

@ -0,0 +1,105 @@
.TH BACKPORTPACKAGE "1" "December 2010" "ubuntu-dev-tools"
.SH NAME
backportpackage \- helper to test package backports
.SH SYNOPSIS
.TP
.B backportpackage \fR[\fIadditional options\fR]
\-\-upload <\fIupload target\fR>
.br
<\fIsource package name or .dsc URL/file\fR>
.PP
.B backportpackage \-h
.SH OPTIONS
.TP
.B \-d \fIDEST\fR, \-\-destination=\fIDEST\fR
\fBRequired\fR. Backport the package to the specified Ubuntu
release. If this option is unspecified, then \fBbackportpackage\fR
defaults to the release on which it is currently running.
.TP
.B \-s \fISOURCE\fR, \-\-source=\fISOURCE\fR
Backport the package from the specified Ubuntu release. If neither
this option nor \fB\-\-version\fR are specified, then
\fBbackportpackage\fR defaults to the current Ubuntu development
release.
.TP
.B \-b, \-\-build
Build the package with the specified builder before uploading. Note
for \fBpbuilder\fR(8) users: This assumes the common configuration,
where the \fBDIST\fR environment is read by \fBpbuilderrc\fR(5) to
select the correct base image.
.TP
.B \-B \fIBUILDER\fR, \fB\-\-builder\fR=\fIBUILDER
Use the specified builder to build the package. Supported are
\fBpbuilder\fR(8) and \fBsbuild\fR(1). This overrides
\fBUBUNTUTOOLS_BUILDER\fR. The default is \fBpbuilder\fR(8).
.TP
.B \-u \fIUPLOAD\fR, \-\-upload=\fIUPLOAD\fR
Upload to \fIUPLOAD\fR with \fBdput\fR(1) (after confirmation).
.TP
.B \-v \fIVERSION\fR, \-\-version=\fIVERSION\fR
If the \fB\-\-source\fR option is specified, then
\fBbackportpackage\fR verifies that the current version of \fIsource
package\fR in \fISOURCE\fR is the same as \fIVERSION\fR. Otherwise,
\fBbackportpackage\fR finds version \fIVERSION\fR of \fIsource
package\fR, regardless of the release in which it was published (or if
that version is still current). This option is ignored if a .dsc URL
or path is passed in instead of a source package name.
.TP
.B \-w \fIWORKDIR\fR, \-\-workdir=\fIWORKDIR\fR
If \fIWORKDIR\fR is specified, then all files are downloaded,
unpacked, built into, and otherwise manipulated in
\fIWORKDIR\fR. Otherwise, a temporary directory is created, which is
deleted before \fIbackportpackage\fR exits.
.TP
.B \-l \fIINSTANCE\fR, \-\-launchpad=\fIINSTANCE\fR
Use the specified instance of Launchpad (e.g. "staging"), instead of
the default of "production".
.SH DESCRIPTION
\fBbackportpackage\fR fetches a package from one Ubuntu release or
from a specified .dsc path or URL and creates a no-change backport of
that package to a previous release, optionally doing a test build of
the package and/or uploading the resulting backport for testing.
.PP
Unless a working directory is specified, the backported package is
fetched and built in a temporary directory in \fB/tmp\fR, which is
removed once the script finishes running.
.PP
\fBbackportpackage\fR is only recommended for testing backports in a
PPA, not uploading backports to the Ubuntu archive.
.SH ENVIRONMENT
.TP
.B UBUNTUTOOLS_BUILDER
The default builder for Ubuntu development tools that support it
(including \fBbackportpackage\fR). Supported are \fBpbuilder\fR(8) and
\fBsbuild\fR(1). If unset and not provided on the command line,
\fBpbuilder\fR(8) is used.
.SH EXAMPLES
Test-build in your PPA a backport of znc from the current development
release to your workstation's release, deleting the build products
afterwards:
.IP
.nf
.B backportpackage -u ppa:\fIuser\fR/\fIppa\fB znc
.fi
.PP
Backport squashfs-tools from Maverick to both Karmic and Lucid and
test-build both locally, leaving all build products in the current
working directory:
.IP
.nf
.B backportpackage -b -s maverick -d karmic -d lucid -w . \\\\
.B " "squashfs-tools
.fi
.PP
Fetch a package from a PPA, backport it to Hardy, then upload it back
to the same PPA:
.IP
.nf
.B backportpackage -d hardy -u ppa:\fIuser\fR/\fIppa\fR \\\\
.B " "https://launchpad.net/\fIsome/file.dsc\fR
.fi
.SH AUTHOR
\fBbackportpackage\fR and this manpage were written by Evan Broder
<evan@ebroder.net>
.PP
Both are released under GNU General Public License, version 2.

View File

@ -61,7 +61,7 @@ by \fBpbuilderrc\fR(5) to select the correct base image.
.B \-B \fIBUILDER\fR, \fB\-\-builder\fR=\fIBUILDER
Use the specify builder to build the package.
Supported are \fBpbuilder\fR(8) and \fBsbuild\fR(1).
This overrides \fBSPONSOR_PATCH_BUILDER\fR.
This overrides \fBUBUNTUTOOLS_BUILDER\fR and \fBSPONSOR_PATCH_BUILDER\fR.
The default is \fBpbuilder\fR(8).
.TP
.BR \-e ", " \-\-edit
@ -90,11 +90,17 @@ Display a help message and exit.
.SH ENVIRONMENT
.TP
.B SPONSOR_PATCH_BUILDER
The default builder for \fBsponsor\-patch\fR.
.B UBUNTUTOOLS_BUILDER
The default builder for Ubuntu development tools that support it (including
\fBsponsor\-patch\fR).
Supported are \fBpbuilder\fR(8) and \fBsbuild\fR(1).
If unset and not provided on the command line, \fBpbuilder\fR(8) is used.
.TP
.B SPONSOR_PATCH_BUILDER
The default builder for \fBsponsor\-patch\fR.
If specified, this overrides \fBUBUNTUTOOLS_BUILDER\fR.
.TP
.B SPONSOR_PATCH_WORKDIR
The default working directory for \fBsponsor\-patch\fR. If unset and not

View File

@ -16,6 +16,7 @@ if os.path.exists(changelog):
setup(name='ubuntu-dev-tools',
version=version,
scripts=['404main',
'backportpackage',
'check-symbols',
'dch-repeat',
'dgetlp',

View File

@ -28,7 +28,9 @@ import debian.deb822
import debian.debian_support
import launchpadlib.launchpad
from ubuntutools.builder import getBuilder
import ubuntutools.update_maintainer
from ubuntutools.logger import Logger
USER_ABORT = 2
@ -57,7 +59,7 @@ class BugTask(object):
dsc_file = None
for url in source_files:
filename = urllib.unquote(os.path.basename(url))
Print.info("Downloading %s..." % (filename))
Logger.info("Downloading %s..." % (filename))
urllib.urlretrieve(url, filename)
if url.endswith(".dsc"):
dsc_file = filename
@ -148,50 +150,6 @@ class BugTask(object):
return self.project == "ubuntu"
class Builder(object):
def __init__(self, name):
self.name = name
cmd = ["dpkg-architecture", "-qDEB_BUILD_ARCH_CPU"]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
self.architecture = process.communicate()[0].strip()
def get_architecture(self):
return self.architecture
def get_name(self):
return self.name
class Pbuilder(Builder):
def __init__(self):
Builder.__init__(self, "pbuilder")
def build(self, dsc_file, dist, result_directory):
# TODO: Do not rely on a specific pbuilder configuration.
cmd = ["sudo", "-E", "DIST=" + dist, "pbuilder", "--build",
"--distribution", dist, "--architecture", self.architecture,
"--buildresult", result_directory, dsc_file]
Print.command(cmd)
return subprocess.call(cmd)
class Sbuild(Builder):
def __init__(self):
Builder.__init__(self, "sbuild")
def build(self, dsc_file, dist, result_directory):
workdir = os.getcwd()
Print.command(["cd", result_directory])
os.chdir(result_directory)
cmd = ["sbuild", "--arch-all", "--dist=" + dist,
"--arch=" + self.architecture, dsc_file]
Print.command(cmd)
result = subprocess.call(cmd)
Print.command(["cd", workdir])
os.chdir(workdir)
return result
class Patch(object):
def __init__(self, patch_file):
self.patch_file = patch_file
@ -220,41 +178,6 @@ class Patch(object):
self.changed_files)) > 0
class Print(object):
script_name = os.path.basename(sys.argv[0])
verbose = False
@classmethod
def command(cls, cmd):
if cls.verbose:
for i in xrange(len(cmd)):
if cmd[i].find(" ") >= 0:
cmd[i] = '"' + cmd[i] + '"'
print "%s: I: %s" % (script_name, " ".join(cmd))
@classmethod
def debug(cls, message):
if cls.verbose:
print "%s: D: %s" % (script_name, message)
@classmethod
def error(cls, message):
print >> sys.stderr, "%s: Error: %s" % (script_name, message)
@classmethod
def info(cls, message):
if cls.verbose:
print "%s: I: %s" % (script_name, message)
@classmethod
def normal(cls, message):
print "%s: %s" % (script_name, message)
@classmethod
def set_verbosity(cls, verbose):
cls.verbose = verbose
def get_source_package_name(bug_task):
package = None
if bug_task.bug_target_name != "ubuntu":
@ -334,7 +257,7 @@ def yes_edit_no_question(question, default):
def edit_source():
# Spawn shell to allow modifications
cmd = [get_user_shell()]
Print.command(cmd)
Logger.command(cmd)
print """An interactive shell was launched in
file://%s
Edit your files. When you are done, exit the shell. If you wish to abort the
@ -342,7 +265,7 @@ process, exit the shell such that it returns an exit code other than zero.
""" % (os.getcwd()),
returncode = subprocess.call(cmd)
if returncode != 0:
Print.error("Shell exited with exit value %i." % (returncode))
Logger.error("Shell exited with exit value %i." % (returncode))
sys.exit(1)
def get_fixed_lauchpad_bugs(changes_file):
@ -380,10 +303,10 @@ def get_patch_or_branch(bug):
linked_branches = map(lambda b: b.branch, bug.linked_branches)
if len(attached_patches) == 0 and len(linked_branches) == 0:
if len(bug.attachments) == 0:
Print.error("No attachment and no linked branch found on bug #%i." \
Logger.error("No attachment and no linked branch found on bug #%i." \
% (bug.id))
else:
Print.error(("No attached patch and no linked branch found. Go " \
Logger.error(("No attached patch and no linked branch found. Go " \
"to https://launchpad.net/bugs/%i and mark an " \
"attachment as patch.") % (bug.id))
sys.exit(1)
@ -393,13 +316,13 @@ def get_patch_or_branch(bug):
branch = linked_branches[0].bzr_identity
else:
if len(attached_patches) == 0:
Print.normal("https://launchpad.net/bugs/%i has %i branches " \
Logger.normal("https://launchpad.net/bugs/%i has %i branches " \
"linked:" % (bug.id, len(linked_branches)))
elif len(linked_branches) == 0:
Print.normal("https://launchpad.net/bugs/%i has %i patches" \
Logger.normal("https://launchpad.net/bugs/%i has %i patches" \
" attached:" % (bug.id, len(attached_patches)))
else:
Print.normal("https://launchpad.net/bugs/%i has %i branch(es)" \
Logger.normal("https://launchpad.net/bugs/%i has %i branch(es)" \
" linked and %i patch(es) attached:" % \
(bug.id, len(linked_branches), len(attached_patches)))
i = 0
@ -421,11 +344,11 @@ def download_patch(patch):
patch_filename = re.sub(" ", "_", patch.title)
if not reduce(lambda r, x: r or patch.title.endswith(x),
(".debdiff", ".diff", ".patch"), False):
Print.info("Patch %s does not have a proper file extension." % \
Logger.info("Patch %s does not have a proper file extension." % \
(patch.title))
patch_filename += ".patch"
Print.info("Downloading %s." % (patch_filename))
Logger.info("Downloading %s." % (patch_filename))
patch_file = open(patch_filename, "w")
patch_file.write(patch.data.open().read())
patch_file.close()
@ -436,18 +359,18 @@ def download_branch(branch):
if os.path.isdir(dir_name):
shutil.rmtree(dir_name)
cmd = ["bzr", "branch", branch]
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd) != 0:
Print.error("Failed to download branch %s." % (branch))
Logger.error("Failed to download branch %s." % (branch))
sys.exit(1)
return dir_name
def merge_branch(branch):
edit = False
cmd = ["bzr", "merge", branch]
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd) != 0:
Print.error("Failed to merge branch %s." % (branch))
Logger.error("Failed to merge branch %s." % (branch))
ask_for_manual_fixing()
edit = True
return edit
@ -456,9 +379,9 @@ def extract_source(dsc_file, verbose=False):
cmd = ["dpkg-source", "--no-preparation", "-x", dsc_file]
if not verbose:
cmd.insert(1, "-q")
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd) != 0:
Print.error("Extraction of %s failed." % (os.path.basename(dsc_file)))
Logger.error("Extraction of %s failed." % (os.path.basename(dsc_file)))
sys.exit(1)
def apply_patch(task, patch):
@ -466,9 +389,9 @@ def apply_patch(task, patch):
if patch.is_debdiff():
cmd = ["patch", "--merge", "--force", "-p",
str(patch.get_strip_level()), "-i", patch.full_path]
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd) != 0:
Print.error("Failed to apply debdiff %s to %s %s." % \
Logger.error("Failed to apply debdiff %s to %s %s." % \
(patch.get_name(), task.package, task.get_version()))
if not edit:
ask_for_manual_fixing()
@ -477,9 +400,9 @@ def apply_patch(task, patch):
# FIXME: edit-patch needs a non-interactive mode
# https://launchpad.net/bugs/612566
cmd = ["edit-patch", patch.full_path]
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd) != 0:
Print.error("Failed to apply diff %s to %s %s." % \
Logger.error("Failed to apply diff %s to %s %s." % \
(patch.get_name(), task.package, task.get_version()))
if not edit:
ask_for_manual_fixing()
@ -493,11 +416,11 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
try:
os.makedirs(workdir)
except os.error, error:
Print.error("Failed to create the working directory %s [Errno " \
Logger.error("Failed to create the working directory %s [Errno " \
"%i]: %s." % (workdir, error.errno, error.strerror))
sys.exit(1)
if workdir != os.getcwd():
Print.command(["cd", workdir])
Logger.command(["cd", workdir])
os.chdir(workdir)
script_name = os.path.basename(sys.argv[0])
@ -510,13 +433,13 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
bug_tasks = map(lambda x: BugTask(x, launchpad), bug.bug_tasks)
ubuntu_tasks = filter(lambda x: x.is_ubuntu_task(), bug_tasks)
if len(ubuntu_tasks) == 0:
Print.error("No Ubuntu bug task found on bug #%i." % (bug_number))
Logger.error("No Ubuntu bug task found on bug #%i." % (bug_number))
sys.exit(1)
elif len(ubuntu_tasks) == 1:
task = ubuntu_tasks[0]
if len(ubuntu_tasks) > 1:
if verbose:
Print.info("%i Ubuntu tasks exist for bug #%i." % \
Logger.info("%i Ubuntu tasks exist for bug #%i." % \
(len(ubuntu_tasks), bug_number))
for task in ubuntu_tasks:
print task.get_short_info()
@ -524,7 +447,7 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
if len(open_ubuntu_tasks) == 1:
task = open_ubuntu_tasks[0]
else:
Print.normal("https://launchpad.net/bugs/%i has %i Ubuntu tasks:" \
Logger.normal("https://launchpad.net/bugs/%i has %i Ubuntu tasks:" \
% (bug_number, len(ubuntu_tasks)))
for i in xrange(len(ubuntu_tasks)):
print "%i) %s" % (i + 1,
@ -532,7 +455,7 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
selected = input_number("To which Ubuntu tasks do the patch belong",
1, len(ubuntu_tasks))
task = ubuntu_tasks[selected - 1]
Print.info("Selected Ubuntu task: %s" % (task.get_short_info()))
Logger.info("Selected Ubuntu task: %s" % (task.get_short_info()))
dsc_file = task.download_source()
assert os.path.isfile(dsc_file), "%s does not exist." % (dsc_file)
@ -540,15 +463,15 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
if patch:
patch = download_patch(patch)
Print.info("Ubuntu package: %s" % (task.package))
Logger.info("Ubuntu package: %s" % (task.package))
if task.is_merge():
Print.info("The task is a merge request.")
Logger.info("The task is a merge request.")
extract_source(dsc_file, verbose)
# change directory
directory = task.package + '-' + task.get_version().upstream_version
Print.command(["cd", directory])
Logger.command(["cd", directory])
os.chdir(directory)
edit |= apply_patch(task, patch)
@ -556,7 +479,7 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
branch_dir = download_branch(task.get_branch_link())
# change directory
Print.command(["cd", branch_dir])
Logger.command(["cd", branch_dir])
os.chdir(branch_dir)
edit |= merge_branch(branch)
@ -568,9 +491,9 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
edit = True
# update the Maintainer field
Print.command(["update-maintainer"])
Logger.command(["update-maintainer"])
if ubuntutools.update_maintainer.update_maintainer(verbose) != 0:
Print.error("update-maintainer script failed.")
Logger.error("update-maintainer script failed.")
sys.exit(1)
# Get new version of package
@ -578,7 +501,7 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
try:
new_version = changelog.get_version()
except IndexError:
Print.error("Debian package version could not be determined. " \
Logger.error("Debian package version could not be determined. " \
"debian/changelog is probably malformed.")
ask_for_manual_fixing()
continue
@ -586,15 +509,15 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
# Check if version of the new package is greater than the version in
# the archive.
if new_version <= task.get_version():
Print.error("The version %s is not greater than the already " \
Logger.error("The version %s is not greater than the already " \
"available %s." % (new_version, task.get_version()))
ask_for_manual_fixing()
continue
cmd = ["dch", "--maintmaint", "--edit", ""]
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd) != 0:
Print.info("Failed to update timestamp in debian/changelog.")
Logger.info("Failed to update timestamp in debian/changelog.")
# Build source package
if patch:
@ -615,9 +538,9 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
env = os.environ
if upload == 'ubuntu':
env['DEB_VENDOR'] = 'Ubuntu'
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd, env=env) != 0:
Print.error("Failed to build source tarball.")
Logger.error("Failed to build source tarball.")
# TODO: Add a "retry" option
ask_for_manual_fixing()
continue
@ -634,7 +557,7 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
debdiff_filename = os.path.join(workdir, debdiff_name)
if not verbose:
cmd.insert(1, "-q")
Print.command(cmd + [">", debdiff_filename])
Logger.command(cmd + [">", debdiff_filename])
process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
debdiff = process.communicate()[0]
@ -646,7 +569,7 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
# Make sure that the Launchpad bug will be closed
changes_file = new_dsc_file[:-4] + "_source.changes"
if bug_number not in get_fixed_lauchpad_bugs(changes_file):
Print.error("Launchpad bug #%i is not closed by new version." % \
Logger.error("Launchpad bug #%i is not closed by new version." % \
(bug_number))
ask_for_manual_fixing()
continue
@ -660,7 +583,7 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
allowed = map(lambda s: s + "-proposed", supported_series) + \
[devel_series]
if changelog.distributions not in allowed:
Print.error("%s is not an allowed series. It needs to be one " \
Logger.error("%s is not an allowed series. It needs to be one " \
"of %s." % (changelog.distributions,
", ".join(allowed)))
ask_for_manual_fixing()
@ -668,7 +591,7 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
elif upload and upload.startwith("ppa/"):
allowed = supported_series + [devel_series]
if changelog.distributions not in allowed:
Print.error("%s is not an allowed series. It needs to be one " \
Logger.error("%s is not an allowed series. It needs to be one " \
"of %s." % (changelog.distributions,
", ".join(allowed)))
ask_for_manual_fixing()
@ -683,7 +606,7 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
dist = re.sub("-.*$", "", changelog.distributions)
result = builder.build(new_dsc_file, dist, buildresult)
if result != 0:
Print.error("Failed to build %s from source with %s." % \
Logger.error("Failed to build %s from source with %s." % \
(os.path.basename(new_dsc_file),
builder.get_name()))
# TODO: Add "retry" and "update" option
@ -699,7 +622,7 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
cmd = ["lintian", "-IE", "--pedantic", "-q", build_changes]
lintian_filename = os.path.join(workdir,
task.package + "_" + strip_epoch(new_version) + ".lintian")
Print.command(cmd + [">", lintian_filename])
Logger.command(cmd + [">", lintian_filename])
process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
report = process.communicate()[0]
@ -727,26 +650,26 @@ def main(script_name, bug_number, build, edit, keyid, upload, workdir, builder,
print "Abort."
sys.exit(USER_ABORT)
cmd = ["dput", "--force", upload, changes_file]
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd) != 0:
Print.error("Upload of %s to %s failed." % \
Logger.error("Upload of %s to %s failed." % \
(os.path.basename(changes_file), upload))
sys.exit(1)
if branch:
cmd = ['debcommit']
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd) != 0:
Print.error('Bzr commit failed.')
Logger.error('Bzr commit failed.')
sys.exit(1)
cmd = ['bzr', 'mark-uploaded']
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd) != 0:
Print.error('Bzr tagging failed.')
Logger.error('Bzr tagging failed.')
sys.exit(1)
cmd = ['bzr', 'push', ':parent']
Print.command(cmd)
Logger.command(cmd)
if subprocess.call(cmd) != 0:
Print.error('Bzr push failed.')
Logger.error('Bzr push failed.')
sys.exit(1)
# Leave while loop if everything worked
@ -766,7 +689,7 @@ if __name__ == "__main__":
if "SPONSOR_PATCH_BUILDER" in os.environ:
default_builder = os.environ["SPONSOR_PATCH_BUILDER"]
else:
default_builder = "pbuilder"
default_builder = None
parser.add_option("-b", "--build", dest="build",
help="Build the package with the specified builder.",
@ -790,29 +713,24 @@ if __name__ == "__main__":
help="Specify a working directory.")
(options, args) = parser.parse_args()
Print.set_verbosity(options.verbose)
Logger.set_verbosity(options.verbose)
if len(args) == 0:
Print.error("No bug number specified.")
Logger.error("No bug number specified.")
sys.exit(1)
elif len(args) > 1:
Print.error("Multiple bug numbers specified: %s" % (", ".join(args)))
Logger.error("Multiple bug numbers specified: %s" % (", ".join(args)))
sys.exit(1)
bug_number = args[0]
if bug_number.isdigit():
bug_number = int(bug_number)
else:
Print.error("Invalid bug number specified: %s" % (bug_number))
Logger.error("Invalid bug number specified: %s" % (bug_number))
sys.exit(1)
if options.builder == "pbuilder":
builder = Pbuilder()
elif options.builder == "sbuild":
builder = Sbuild()
else:
Print.error("Unsupported builder specified: %s. Only pbuilder and "
"sbuild are supported." % (options.builder))
builder = getBuilder(options.builder)
if not builder:
sys.exit(1)
if options.sponsoring:

81
ubuntutools/builder.py Normal file
View File

@ -0,0 +1,81 @@
#
# builder.py - Helper classes for building packages
#
# Copyright (C) 2010, Benjamin Drung <bdrung@ubuntu.com>
# Copyright (C) 2010, Evan Broder <evan@ebroder.net>
#
# Permission to use, copy, modify, and/or distribute this software
# for any purpose with or without fee is hereby granted, provided
# that the above copyright notice and this permission notice appear
# in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
import os
import subprocess
from ubuntutools.logger import Logger
class Builder(object):
def __init__(self, name):
self.name = name
cmd = ["dpkg-architecture", "-qDEB_BUILD_ARCH_CPU"]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
self.architecture = process.communicate()[0].strip()
def get_architecture(self):
return self.architecture
def get_name(self):
return self.name
class Pbuilder(Builder):
def __init__(self):
Builder.__init__(self, "pbuilder")
def build(self, dsc_file, dist, result_directory):
# TODO: Do not rely on a specific pbuilder configuration.
cmd = ["sudo", "-E", "DIST=" + dist, "pbuilder", "--build",
"--distribution", dist, "--architecture", self.architecture,
"--buildresult", result_directory, dsc_file]
Logger.command(cmd)
return subprocess.call(cmd)
class Sbuild(Builder):
def __init__(self):
Builder.__init__(self, "sbuild")
def build(self, dsc_file, dist, result_directory):
workdir = os.getcwd()
Logger.command(["cd", result_directory])
os.chdir(result_directory)
cmd = ["sbuild", "--arch-all", "--dist=" + dist,
"--arch=" + self.architecture, dsc_file]
Logger.command(cmd)
result = subprocess.call(cmd)
Logger.command(["cd", workdir])
os.chdir(workdir)
return result
def getBuilder(builder=None):
if not builder:
builder = os.environ.get('UBUNTUTOOLS_BUILDER', 'pbuilder')
if builder == 'pbuilder':
return Pbuilder()
elif builder == 'sbuild':
return Sbuild()
Logger.error("Unsupported builder specified: %s. Only pbuilder and "
"sbuild are supported." % builder)

55
ubuntutools/logger.py Normal file
View File

@ -0,0 +1,55 @@
#
# logger.py - A simple logging helper class
#
# Copyright (C) 2010, Benjamin Drung <bdrung@ubuntu.com>
#
# Permission to use, copy, modify, and/or distribute this software
# for any purpose with or without fee is hereby granted, provided
# that the above copyright notice and this permission notice appear
# in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import os
import sys
class Logger(object):
script_name = os.path.basename(sys.argv[0])
verbose = False
@classmethod
def command(cls, cmd):
if cls.verbose:
for i in xrange(len(cmd)):
if cmd[i].find(" ") >= 0:
cmd[i] = '"' + cmd[i] + '"'
print "%s: I: %s" % (cls.script_name, " ".join(cmd))
@classmethod
def debug(cls, message):
if cls.verbose:
print "%s: D: %s" % (cls.script_name, message)
@classmethod
def error(cls, message):
print >> sys.stderr, "%s: Error: %s" % (cls.script_name, message)
@classmethod
def info(cls, message):
if cls.verbose:
print "%s: I: %s" % (cls.script_name, message)
@classmethod
def normal(cls, message):
print "%s: %s" % (cls.script_name, message)
@classmethod
def set_verbosity(cls, verbose):
cls.verbose = verbose