diff --git a/backportpackage b/backportpackage new file mode 100755 index 0000000..d8c659e --- /dev/null +++ b/backportpackage @@ -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] ' + 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)) diff --git a/debian/changelog b/debian/changelog index 8445f16..0b7f708 100644 --- a/debian/changelog +++ b/debian/changelog @@ -12,7 +12,10 @@ ubuntu-dev-tools (0.108) UNRELEASED; urgency=low [ Colin Watson ] * grep-merges: New tool. - -- Benjamin Drung Tue, 14 Dec 2010 18:21:37 +0100 + [ Evan Broder ] + * backportpackage: new script for testing backport requests in a PPA. + + -- Benjamin Drung Thu, 16 Dec 2010 23:40:14 +0100 ubuntu-dev-tools (0.107) experimental; urgency=low diff --git a/doc/backportpackage.1 b/doc/backportpackage.1 new file mode 100644 index 0000000..8d30e88 --- /dev/null +++ b/doc/backportpackage.1 @@ -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 + +.PP +Both are released under GNU General Public License, version 2. diff --git a/doc/sponsor-patch.1 b/doc/sponsor-patch.1 index 5a59f26..d1da9ec 100644 --- a/doc/sponsor-patch.1 +++ b/doc/sponsor-patch.1 @@ -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 diff --git a/setup.py b/setup.py index 5e3675d..e11e7c0 100755 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ if os.path.exists(changelog): setup(name='ubuntu-dev-tools', version=version, scripts=['404main', + 'backportpackage', 'check-symbols', 'dch-repeat', 'dgetlp', diff --git a/sponsor-patch b/sponsor-patch index 0f6ca7c..0d8cde6 100755 --- a/sponsor-patch +++ b/sponsor-patch @@ -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: diff --git a/ubuntutools/builder.py b/ubuntutools/builder.py new file mode 100644 index 0000000..e15a534 --- /dev/null +++ b/ubuntutools/builder.py @@ -0,0 +1,81 @@ +# +# builder.py - Helper classes for building packages +# +# Copyright (C) 2010, Benjamin Drung +# Copyright (C) 2010, Evan Broder +# +# 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) diff --git a/ubuntutools/logger.py b/ubuntutools/logger.py new file mode 100644 index 0000000..db6a016 --- /dev/null +++ b/ubuntutools/logger.py @@ -0,0 +1,55 @@ +# +# logger.py - A simple logging helper class +# +# Copyright (C) 2010, Benjamin Drung +# +# 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