From 9e6a13f5a970718ceff8680521538427482f0ec3 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Fri, 17 Dec 2010 17:47:23 +0100 Subject: [PATCH] Split main function out of sponsor-patch script. --- sponsor-patch | 579 +++--------------------------- ubuntutools/sponsor_patch/main.py | 482 +++++++++++++++++++++++++ 2 files changed, 540 insertions(+), 521 deletions(-) create mode 100644 ubuntutools/sponsor_patch/main.py diff --git a/sponsor-patch b/sponsor-patch index e83804c..49000a5 100755 --- a/sponsor-patch +++ b/sponsor-patch @@ -16,538 +16,75 @@ import optparse import os -import pwd -import re -import shutil -import subprocess import sys -import debian.changelog -import debian.deb822 -import launchpadlib.launchpad - from ubuntutools.builder import getBuilder -import ubuntutools.update_maintainer from ubuntutools.logger import Logger -from ubuntutools.question import input_number, boolean_question, yes_edit_no_question +from ubuntutools.sponsor_patch.main import main -from ubuntutools.sponsor_patch.bugtask import BugTask -from ubuntutools.sponsor_patch.patch import Patch +script_name = os.path.basename(sys.argv[0]) +usage = "%s [options] " % (script_name) +epilog = "See %s(1) for more info." % (script_name) +parser = optparse.OptionParser(usage=usage, epilog=epilog) -USER_ABORT = 2 +if "SPONSOR_PATCH_WORKDIR" in os.environ: + default_workdir = os.path.abspath(os.environ["SPONSOR_PATCH_WORKDIR"]) +else: + default_workdir = os.getcwd() -def get_source_package_name(bug_task): - package = None - if bug_task.bug_target_name != "ubuntu": - assert bug_task.bug_target_name.endswith("(Ubuntu)") - package = bug_task.bug_target_name.split(" ")[0] - return package +if "SPONSOR_PATCH_BUILDER" in os.environ: + default_builder = os.environ["SPONSOR_PATCH_BUILDER"] +else: + default_builder = None -def get_user_shell(): - try: - shell = os.environ["SHELL"] - except KeyError: - shell = pwd.getpwuid(os.getuid())[6] - return shell +parser.add_option("-b", "--build", dest="build", + help="Build the package with the specified builder.", + action="store_true", default=False) +parser.add_option("-B", "--builder", dest="builder", + help="Specify the package builder (default pbuilder)", + default=default_builder) +parser.add_option("-e", "--edit", + help="launch sub-shell to allow editing of the patch", + dest="edit", action="store_true", default=False) +parser.add_option("-k", "--key", dest="keyid", default=None, + help="Specify the key ID to be used for signing.") +parser.add_option("-s", "--sponsor", help="sponsoring; equals -b -u ubuntu", + dest="sponsoring", action="store_true", default=False) +parser.add_option("-u", "--upload", dest="upload", default=None, + help="Specify an upload destination (default none).") +parser.add_option("-U", "--update", dest="update", default=False, + action="store_true", + help="Update the build environment before building.") +parser.add_option("-v", "--verbose", help="print more information", + dest="verbose", action="store_true", default=False) +parser.add_option("-w", "--workdir", dest="workdir", + default=default_workdir, + help="Specify a working directory.") -def edit_source(): - # Spawn shell to allow modifications - cmd = [get_user_shell()] - 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 -process, exit the shell such that it returns an exit code other than zero. -""" % (os.getcwd()), - returncode = subprocess.call(cmd) - if returncode != 0: - Logger.error("Shell exited with exit value %i." % (returncode)) - sys.exit(1) +(options, args) = parser.parse_args() +Logger.set_verbosity(options.verbose) -def get_fixed_lauchpad_bugs(changes_file): - assert os.path.isfile(changes_file), "%s does not exist." % (changes_file) - changes = debian.deb822.Changes(file(changes_file)) - fixed_bugs = [] - if "Launchpad-Bugs-Fixed" in changes: - fixed_bugs = changes["Launchpad-Bugs-Fixed"].split(" ") - fixed_bugs = map(int, fixed_bugs) - return fixed_bugs +if len(args) == 0: + Logger.error("No bug number specified.") + sys.exit(1) +elif len(args) > 1: + Logger.error("Multiple bug numbers specified: %s" % (", ".join(args))) + sys.exit(1) -def strip_epoch(version): - """Removes the epoch from a Debian version string. +bug_number = args[0] +if bug_number.isdigit(): + bug_number = int(bug_number) +else: + Logger.error("Invalid bug number specified: %s" % (bug_number)) + sys.exit(1) - strip_epoch(1:1.52-1) will return "1.52-1" and strip_epoch(1.1.3-1) will - return "1.1.3-1". +builder = getBuilder(options.builder) +if not builder: + sys.exit(1) - """ +if options.sponsoring: + options.build = True + options.upload = "ubuntu" - parts = version.full_version.split(':') - if len(parts) > 1: - del parts[0] - version_without_epoch = ':'.join(parts) - return version_without_epoch - -def ask_for_manual_fixing(): - if not boolean_question("Do you want to resolve this issue manually", True): - print "Abort." - sys.exit(USER_ABORT) - -def get_patch_or_branch(bug): - patch = None - branch = None - attached_patches = filter(lambda a: a.type == "Patch", bug.attachments) - 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: - Logger.error("No attachment and no linked branch found on bug #%i." \ - % (bug.id)) - else: - 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) - elif len(attached_patches) == 1 and len(linked_branches) == 0: - patch = attached_patches[0] - elif len(attached_patches) == 0 and len(linked_branches) == 1: - branch = linked_branches[0].bzr_identity - else: - if len(attached_patches) == 0: - Logger.normal("https://launchpad.net/bugs/%i has %i branches " \ - "linked:" % (bug.id, len(linked_branches))) - elif len(linked_branches) == 0: - Logger.normal("https://launchpad.net/bugs/%i has %i patches" \ - " attached:" % (bug.id, len(attached_patches))) - else: - 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 - for linked_branch in linked_branches: - i += 1 - print "%i) %s" % (i, linked_branch.display_name) - for attached_patch in attached_patches: - i += 1 - print "%i) %s" % (i, attached_patch.title) - selected = input_number("Which branch or patch do you want to download", - 1, i, i) - if selected <= len(linked_branches): - branch = linked_branches[selected - 1].bzr_identity - else: - patch = attached_patches[selected - len(linked_branches) - 1] - return (patch, branch) - -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): - Logger.info("Patch %s does not have a proper file extension." % \ - (patch.title)) - patch_filename += ".patch" - - Logger.info("Downloading %s." % (patch_filename)) - patch_file = open(patch_filename, "w") - patch_file.write(patch.data.open().read()) - patch_file.close() - return Patch(patch_filename) - -def download_branch(branch): - dir_name = os.path.basename(branch) - if os.path.isdir(dir_name): - shutil.rmtree(dir_name) - cmd = ["bzr", "branch", branch] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error("Failed to download branch %s." % (branch)) - sys.exit(1) - return dir_name - -def merge_branch(branch): - edit = False - cmd = ["bzr", "merge", branch] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error("Failed to merge branch %s." % (branch)) - ask_for_manual_fixing() - edit = True - return edit - -def extract_source(dsc_file, verbose=False): - cmd = ["dpkg-source", "--no-preparation", "-x", dsc_file] - if not verbose: - cmd.insert(1, "-q") - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error("Extraction of %s failed." % (os.path.basename(dsc_file))) - sys.exit(1) - -def apply_patch(task, patch): - edit = False - if patch.is_debdiff(): - cmd = ["patch", "--merge", "--force", "-p", - str(patch.get_strip_level()), "-i", patch.full_path] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - 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() - edit = True - else: - # FIXME: edit-patch needs a non-interactive mode - # https://launchpad.net/bugs/612566 - cmd = ["edit-patch", patch.full_path] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - 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() - edit = True - return edit - -def main(script_name, bug_number, update, build, edit, keyid, upload, workdir, - builder, verbose=False): - workdir = os.path.expanduser(workdir) - if not os.path.isdir(workdir): - try: - os.makedirs(workdir) - except os.error, error: - Logger.error("Failed to create the working directory %s [Errno " \ - "%i]: %s." % (workdir, error.errno, error.strerror)) - sys.exit(1) - if workdir != os.getcwd(): - Logger.command(["cd", workdir]) - os.chdir(workdir) - - script_name = os.path.basename(sys.argv[0]) - launchpad = launchpadlib.launchpad.Launchpad.login_anonymously(script_name, - "production") - bug = launchpad.bugs[bug_number] - - (patch, branch) = get_patch_or_branch(bug) - - 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: - 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: - Logger.info("%i Ubuntu tasks exist for bug #%i." % \ - (len(ubuntu_tasks), bug_number)) - for task in ubuntu_tasks: - print task.get_short_info() - open_ubuntu_tasks = filter(lambda x: not x.is_complete(), ubuntu_tasks) - if len(open_ubuntu_tasks) == 1: - task = open_ubuntu_tasks[0] - else: - 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, - ubuntu_tasks[i].get_package_and_series()) - selected = input_number("To which Ubuntu tasks do the patch belong", - 1, len(ubuntu_tasks)) - task = ubuntu_tasks[selected - 1] - 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) - - if patch: - patch = download_patch(patch) - - Logger.info("Ubuntu package: %s" % (task.package)) - if task.is_merge(): - Logger.info("The task is a merge request.") - - extract_source(dsc_file, verbose) - - # change directory - directory = task.package + '-' + task.get_version().upstream_version - Logger.command(["cd", directory]) - os.chdir(directory) - - edit |= apply_patch(task, patch) - elif branch: - branch_dir = download_branch(task.get_branch_link()) - - # change directory - Logger.command(["cd", branch_dir]) - os.chdir(branch_dir) - - edit |= merge_branch(branch) - - while True: - if edit: - edit_source() - # All following loop executions require manual editing. - edit = True - - # update the Maintainer field - Logger.command(["update-maintainer"]) - if ubuntutools.update_maintainer.update_maintainer(verbose) != 0: - Logger.error("update-maintainer script failed.") - sys.exit(1) - - # Get new version of package - changelog = debian.changelog.Changelog(file("debian/changelog")) - try: - new_version = changelog.get_version() - except IndexError: - Logger.error("Debian package version could not be determined. " \ - "debian/changelog is probably malformed.") - ask_for_manual_fixing() - continue - - # Check if version of the new package is greater than the version in - # the archive. - if new_version <= task.get_version(): - 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", ""] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.info("Failed to update timestamp in debian/changelog.") - - # Build source package - if patch: - cmd = ['debuild', '--no-lintian', '-S'] - elif branch: - cmd = ['bzr', 'bd', '-S', '--', '--no-lintian'] - previous_version = task.get_previous_version() - cmd.append("-v" + previous_version.full_version) - if previous_version.upstream_version == changelog.upstream_version and \ - upload == "ubuntu": - # FIXME: Add proper check that catches cases like changed - # compression (.tar.gz -> tar.bz2) and multiple orig source tarballs - cmd.append("-sd") - else: - cmd.append("-sa") - if not keyid is None: - cmd += ["-k" + keyid] - env = os.environ - if upload == 'ubuntu': - env['DEB_VENDOR'] = 'Ubuntu' - Logger.command(cmd) - if subprocess.call(cmd, env=env) != 0: - Logger.error("Failed to build source tarball.") - # TODO: Add a "retry" option - ask_for_manual_fixing() - continue - - # Generate debdiff - new_dsc_file = os.path.join(workdir, - task.package + "_" + strip_epoch(new_version) + ".dsc") - assert os.path.isfile(dsc_file), "%s does not exist." % (dsc_file) - assert os.path.isfile(new_dsc_file), "%s does not exist." % \ - (new_dsc_file) - cmd = ["debdiff", dsc_file, new_dsc_file] - debdiff_name = task.package + "_" + strip_epoch(new_version) + \ - ".debdiff" - debdiff_filename = os.path.join(workdir, debdiff_name) - if not verbose: - cmd.insert(1, "-q") - Logger.command(cmd + [">", debdiff_filename]) - process = subprocess.Popen(cmd, stdout=subprocess.PIPE) - debdiff = process.communicate()[0] - - # write debdiff file - debdiff_file = open(debdiff_filename, "w") - debdiff_file.writelines(debdiff) - debdiff_file.close() - - # 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): - Logger.error("Launchpad bug #%i is not closed by new version." % \ - (bug_number)) - ask_for_manual_fixing() - continue - - ubuntu = launchpad.distributions['ubuntu'] - devel_series = ubuntu.current_series.name - supported_series = [series.name for series in ubuntu.series - if series.active and series.name != devel_series] - # Make sure that the target is correct - if upload == "ubuntu": - allowed = map(lambda s: s + "-proposed", supported_series) + \ - [devel_series] - if changelog.distributions not in allowed: - Logger.error("%s is not an allowed series. It needs to be one " \ - "of %s." % (changelog.distributions, - ", ".join(allowed))) - ask_for_manual_fixing() - continue - elif upload and upload.startwith("ppa/"): - allowed = supported_series + [devel_series] - if changelog.distributions not in allowed: - Logger.error("%s is not an allowed series. It needs to be one " \ - "of %s." % (changelog.distributions, - ", ".join(allowed))) - ask_for_manual_fixing() - continue - - if build: - dist = re.sub("-.*$", "", changelog.distributions) - - if update: - ret = builder.update(dist) - if ret != 0: - Logger.error("Failed to update %s chroot for %s." % \ - (dist, builder.get_name())) - ask_for_manual_fixing() - continue - # We want to update the build environment only once, but not - # after every manual fix. - update = False - - buildresult = os.path.join(workdir, task.package + "-buildresult") - if not os.path.isdir(buildresult): - os.makedirs(buildresult) - - # build package - result = builder.build(new_dsc_file, dist, buildresult) - if result != 0: - 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 - ask_for_manual_fixing() - continue - - # Check lintian - changes_name = task.package + "_" + strip_epoch(new_version) + \ - "_" + builder.get_architecture() + ".changes" - build_changes = os.path.join(buildresult, changes_name) - assert os.path.isfile(build_changes), "%s does not exist." % \ - (build_changes) - cmd = ["lintian", "-IE", "--pedantic", "-q", build_changes] - lintian_filename = os.path.join(workdir, - task.package + "_" + strip_epoch(new_version) + ".lintian") - Logger.command(cmd + [">", lintian_filename]) - process = subprocess.Popen(cmd, stdout=subprocess.PIPE) - report = process.communicate()[0] - - # write lintian report file - lintian_file = open(lintian_filename, "w") - lintian_file.writelines(report) - lintian_file.close() - - # Upload package - if upload: - if upload == "ubuntu": - build_name = task.package + "_" + strip_epoch(new_version) + \ - "_" + builder.get_architecture() + ".build" - build_log = os.path.join(buildresult, build_name) - print "Please check %s %s carefully:\nfile://%s\nfile://%s\n" \ - "file://%s" % (task.package, new_version, - debdiff_filename, lintian_filename, - build_log) - answer = yes_edit_no_question("Do you want to upload the " \ - "package to the official " \ - "Ubuntu archive", "yes") - if answer == "edit": - continue - elif answer == "no": - print "Abort." - sys.exit(USER_ABORT) - cmd = ["dput", "--force", upload, changes_file] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error("Upload of %s to %s failed." % \ - (os.path.basename(changes_file), upload)) - sys.exit(1) - if branch: - cmd = ['debcommit'] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error('Bzr commit failed.') - sys.exit(1) - cmd = ['bzr', 'mark-uploaded'] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error('Bzr tagging failed.') - sys.exit(1) - cmd = ['bzr', 'push', ':parent'] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error('Bzr push failed.') - sys.exit(1) - - # Leave while loop if everything worked - break - -if __name__ == "__main__": - script_name = os.path.basename(sys.argv[0]) - usage = "%s [options] " % (script_name) - epilog = "See %s(1) for more info." % (script_name) - parser = optparse.OptionParser(usage=usage, epilog=epilog) - - if "SPONSOR_PATCH_WORKDIR" in os.environ: - default_workdir = os.path.abspath(os.environ["SPONSOR_PATCH_WORKDIR"]) - else: - default_workdir = os.getcwd() - - if "SPONSOR_PATCH_BUILDER" in os.environ: - default_builder = os.environ["SPONSOR_PATCH_BUILDER"] - else: - default_builder = None - - parser.add_option("-b", "--build", dest="build", - help="Build the package with the specified builder.", - action="store_true", default=False) - parser.add_option("-B", "--builder", dest="builder", - help="Specify the package builder (default pbuilder)", - default=default_builder) - parser.add_option("-e", "--edit", - help="launch sub-shell to allow editing of the patch", - dest="edit", action="store_true", default=False) - parser.add_option("-k", "--key", dest="keyid", default=None, - help="Specify the key ID to be used for signing.") - parser.add_option("-s", "--sponsor", help="sponsoring; equals -b -u ubuntu", - dest="sponsoring", action="store_true", default=False) - parser.add_option("-u", "--upload", dest="upload", default=None, - help="Specify an upload destination (default none).") - parser.add_option("-U", "--update", dest="update", default=False, - action="store_true", - help="Update the build environment before building.") - parser.add_option("-v", "--verbose", help="print more information", - dest="verbose", action="store_true", default=False) - parser.add_option("-w", "--workdir", dest="workdir", - default=default_workdir, - help="Specify a working directory.") - - (options, args) = parser.parse_args() - Logger.set_verbosity(options.verbose) - - if len(args) == 0: - Logger.error("No bug number specified.") - sys.exit(1) - elif len(args) > 1: - 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: - Logger.error("Invalid bug number specified: %s" % (bug_number)) - sys.exit(1) - - builder = getBuilder(options.builder) - if not builder: - sys.exit(1) - - if options.sponsoring: - options.build = True - options.upload = "ubuntu" - - main(script_name, bug_number, options.update, options.build, options.edit, - options.keyid, options.upload, options.workdir, builder, - options.verbose) +main(bug_number, options.update, options.build, options.edit, options.keyid, + options.upload, options.workdir, builder, options.verbose) diff --git a/ubuntutools/sponsor_patch/main.py b/ubuntutools/sponsor_patch/main.py new file mode 100644 index 0000000..19753c8 --- /dev/null +++ b/ubuntutools/sponsor_patch/main.py @@ -0,0 +1,482 @@ +# +# main.py - main function for sponsor-patch script +# +# 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 pwd +import re +import shutil +import subprocess +import sys + +import debian.changelog +import debian.deb822 +import launchpadlib.launchpad + +import ubuntutools.update_maintainer +from ubuntutools.logger import Logger +from ubuntutools.question import input_number, boolean_question, yes_edit_no_question + +from ubuntutools.sponsor_patch.bugtask import BugTask +from ubuntutools.sponsor_patch.patch import Patch + +USER_ABORT = 2 + +def get_source_package_name(bug_task): + package = None + if bug_task.bug_target_name != "ubuntu": + assert bug_task.bug_target_name.endswith("(Ubuntu)") + package = bug_task.bug_target_name.split(" ")[0] + return package + +def get_user_shell(): + try: + shell = os.environ["SHELL"] + except KeyError: + shell = pwd.getpwuid(os.getuid())[6] + return shell + +def edit_source(): + # Spawn shell to allow modifications + cmd = [get_user_shell()] + 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 +process, exit the shell such that it returns an exit code other than zero. +""" % (os.getcwd()), + returncode = subprocess.call(cmd) + if returncode != 0: + Logger.error("Shell exited with exit value %i." % (returncode)) + sys.exit(1) + +def get_fixed_lauchpad_bugs(changes_file): + assert os.path.isfile(changes_file), "%s does not exist." % (changes_file) + changes = debian.deb822.Changes(file(changes_file)) + fixed_bugs = [] + if "Launchpad-Bugs-Fixed" in changes: + fixed_bugs = changes["Launchpad-Bugs-Fixed"].split(" ") + fixed_bugs = map(int, fixed_bugs) + return fixed_bugs + +def strip_epoch(version): + """Removes the epoch from a Debian version string. + + strip_epoch(1:1.52-1) will return "1.52-1" and strip_epoch(1.1.3-1) will + return "1.1.3-1". + + """ + + parts = version.full_version.split(':') + if len(parts) > 1: + del parts[0] + version_without_epoch = ':'.join(parts) + return version_without_epoch + +def ask_for_manual_fixing(): + if not boolean_question("Do you want to resolve this issue manually", True): + print "Abort." + sys.exit(USER_ABORT) + +def get_patch_or_branch(bug): + patch = None + branch = None + attached_patches = filter(lambda a: a.type == "Patch", bug.attachments) + 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: + Logger.error("No attachment and no linked branch found on bug #%i." \ + % (bug.id)) + else: + 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) + elif len(attached_patches) == 1 and len(linked_branches) == 0: + patch = attached_patches[0] + elif len(attached_patches) == 0 and len(linked_branches) == 1: + branch = linked_branches[0].bzr_identity + else: + if len(attached_patches) == 0: + Logger.normal("https://launchpad.net/bugs/%i has %i branches " \ + "linked:" % (bug.id, len(linked_branches))) + elif len(linked_branches) == 0: + Logger.normal("https://launchpad.net/bugs/%i has %i patches" \ + " attached:" % (bug.id, len(attached_patches))) + else: + 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 + for linked_branch in linked_branches: + i += 1 + print "%i) %s" % (i, linked_branch.display_name) + for attached_patch in attached_patches: + i += 1 + print "%i) %s" % (i, attached_patch.title) + selected = input_number("Which branch or patch do you want to download", + 1, i, i) + if selected <= len(linked_branches): + branch = linked_branches[selected - 1].bzr_identity + else: + patch = attached_patches[selected - len(linked_branches) - 1] + return (patch, branch) + +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): + Logger.info("Patch %s does not have a proper file extension." % \ + (patch.title)) + patch_filename += ".patch" + + Logger.info("Downloading %s." % (patch_filename)) + patch_file = open(patch_filename, "w") + patch_file.write(patch.data.open().read()) + patch_file.close() + return Patch(patch_filename) + +def download_branch(branch): + dir_name = os.path.basename(branch) + if os.path.isdir(dir_name): + shutil.rmtree(dir_name) + cmd = ["bzr", "branch", branch] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error("Failed to download branch %s." % (branch)) + sys.exit(1) + return dir_name + +def merge_branch(branch): + edit = False + cmd = ["bzr", "merge", branch] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error("Failed to merge branch %s." % (branch)) + ask_for_manual_fixing() + edit = True + return edit + +def extract_source(dsc_file, verbose=False): + cmd = ["dpkg-source", "--no-preparation", "-x", dsc_file] + if not verbose: + cmd.insert(1, "-q") + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error("Extraction of %s failed." % (os.path.basename(dsc_file))) + sys.exit(1) + +def apply_patch(task, patch): + edit = False + if patch.is_debdiff(): + cmd = ["patch", "--merge", "--force", "-p", + str(patch.get_strip_level()), "-i", patch.full_path] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + 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() + edit = True + else: + # FIXME: edit-patch needs a non-interactive mode + # https://launchpad.net/bugs/612566 + cmd = ["edit-patch", patch.full_path] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + 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() + edit = True + return edit + +def main(bug_number, update, build, edit, keyid, upload, workdir, builder, + verbose=False): + workdir = os.path.expanduser(workdir) + if not os.path.isdir(workdir): + try: + os.makedirs(workdir) + except os.error, error: + Logger.error("Failed to create the working directory %s [Errno " \ + "%i]: %s." % (workdir, error.errno, error.strerror)) + sys.exit(1) + if workdir != os.getcwd(): + Logger.command(["cd", workdir]) + os.chdir(workdir) + + lp = launchpadlib.launchpad.Launchpad.login_anonymously("sponsor-patch", + "production") + bug = lp.bugs[bug_number] + + (patch, branch) = get_patch_or_branch(bug) + + bug_tasks = map(lambda x: BugTask(x, lp), bug.bug_tasks) + ubuntu_tasks = filter(lambda x: x.is_ubuntu_task(), bug_tasks) + if len(ubuntu_tasks) == 0: + 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: + Logger.info("%i Ubuntu tasks exist for bug #%i." % \ + (len(ubuntu_tasks), bug_number)) + for task in ubuntu_tasks: + print task.get_short_info() + open_ubuntu_tasks = filter(lambda x: not x.is_complete(), ubuntu_tasks) + if len(open_ubuntu_tasks) == 1: + task = open_ubuntu_tasks[0] + else: + 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, + ubuntu_tasks[i].get_package_and_series()) + selected = input_number("To which Ubuntu tasks do the patch belong", + 1, len(ubuntu_tasks)) + task = ubuntu_tasks[selected - 1] + 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) + + if patch: + patch = download_patch(patch) + + Logger.info("Ubuntu package: %s" % (task.package)) + if task.is_merge(): + Logger.info("The task is a merge request.") + + extract_source(dsc_file, verbose) + + # change directory + directory = task.package + '-' + task.get_version().upstream_version + Logger.command(["cd", directory]) + os.chdir(directory) + + edit |= apply_patch(task, patch) + elif branch: + branch_dir = download_branch(task.get_branch_link()) + + # change directory + Logger.command(["cd", branch_dir]) + os.chdir(branch_dir) + + edit |= merge_branch(branch) + + while True: + if edit: + edit_source() + # All following loop executions require manual editing. + edit = True + + # update the Maintainer field + Logger.command(["update-maintainer"]) + if ubuntutools.update_maintainer.update_maintainer(verbose) != 0: + Logger.error("update-maintainer script failed.") + sys.exit(1) + + # Get new version of package + changelog = debian.changelog.Changelog(file("debian/changelog")) + try: + new_version = changelog.get_version() + except IndexError: + Logger.error("Debian package version could not be determined. " \ + "debian/changelog is probably malformed.") + ask_for_manual_fixing() + continue + + # Check if version of the new package is greater than the version in + # the archive. + if new_version <= task.get_version(): + 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", ""] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.info("Failed to update timestamp in debian/changelog.") + + # Build source package + if patch: + cmd = ['debuild', '--no-lintian', '-S'] + elif branch: + cmd = ['bzr', 'bd', '-S', '--', '--no-lintian'] + previous_version = task.get_previous_version() + cmd.append("-v" + previous_version.full_version) + if previous_version.upstream_version == changelog.upstream_version and \ + upload == "ubuntu": + # FIXME: Add proper check that catches cases like changed + # compression (.tar.gz -> tar.bz2) and multiple orig source tarballs + cmd.append("-sd") + else: + cmd.append("-sa") + if not keyid is None: + cmd += ["-k" + keyid] + env = os.environ + if upload == 'ubuntu': + env['DEB_VENDOR'] = 'Ubuntu' + Logger.command(cmd) + if subprocess.call(cmd, env=env) != 0: + Logger.error("Failed to build source tarball.") + # TODO: Add a "retry" option + ask_for_manual_fixing() + continue + + # Generate debdiff + new_dsc_file = os.path.join(workdir, + task.package + "_" + strip_epoch(new_version) + ".dsc") + assert os.path.isfile(dsc_file), "%s does not exist." % (dsc_file) + assert os.path.isfile(new_dsc_file), "%s does not exist." % \ + (new_dsc_file) + cmd = ["debdiff", dsc_file, new_dsc_file] + debdiff_name = task.package + "_" + strip_epoch(new_version) + \ + ".debdiff" + debdiff_filename = os.path.join(workdir, debdiff_name) + if not verbose: + cmd.insert(1, "-q") + Logger.command(cmd + [">", debdiff_filename]) + process = subprocess.Popen(cmd, stdout=subprocess.PIPE) + debdiff = process.communicate()[0] + + # write debdiff file + debdiff_file = open(debdiff_filename, "w") + debdiff_file.writelines(debdiff) + debdiff_file.close() + + # 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): + Logger.error("Launchpad bug #%i is not closed by new version." % \ + (bug_number)) + ask_for_manual_fixing() + continue + + ubuntu = lp.distributions['ubuntu'] + devel_series = ubuntu.current_series.name + supported_series = [series.name for series in ubuntu.series + if series.active and series.name != devel_series] + # Make sure that the target is correct + if upload == "ubuntu": + allowed = map(lambda s: s + "-proposed", supported_series) + \ + [devel_series] + if changelog.distributions not in allowed: + Logger.error("%s is not an allowed series. It needs to be one " \ + "of %s." % (changelog.distributions, + ", ".join(allowed))) + ask_for_manual_fixing() + continue + elif upload and upload.startwith("ppa/"): + allowed = supported_series + [devel_series] + if changelog.distributions not in allowed: + Logger.error("%s is not an allowed series. It needs to be one " \ + "of %s." % (changelog.distributions, + ", ".join(allowed))) + ask_for_manual_fixing() + continue + + if build: + dist = re.sub("-.*$", "", changelog.distributions) + + if update: + ret = builder.update(dist) + if ret != 0: + Logger.error("Failed to update %s chroot for %s." % \ + (dist, builder.get_name())) + ask_for_manual_fixing() + continue + # We want to update the build environment only once, but not + # after every manual fix. + update = False + + buildresult = os.path.join(workdir, task.package + "-buildresult") + if not os.path.isdir(buildresult): + os.makedirs(buildresult) + + # build package + result = builder.build(new_dsc_file, dist, buildresult) + if result != 0: + 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 + ask_for_manual_fixing() + continue + + # Check lintian + changes_name = task.package + "_" + strip_epoch(new_version) + \ + "_" + builder.get_architecture() + ".changes" + build_changes = os.path.join(buildresult, changes_name) + assert os.path.isfile(build_changes), "%s does not exist." % \ + (build_changes) + cmd = ["lintian", "-IE", "--pedantic", "-q", build_changes] + lintian_filename = os.path.join(workdir, + task.package + "_" + strip_epoch(new_version) + ".lintian") + Logger.command(cmd + [">", lintian_filename]) + process = subprocess.Popen(cmd, stdout=subprocess.PIPE) + report = process.communicate()[0] + + # write lintian report file + lintian_file = open(lintian_filename, "w") + lintian_file.writelines(report) + lintian_file.close() + + # Upload package + if upload: + if upload == "ubuntu": + build_name = task.package + "_" + strip_epoch(new_version) + \ + "_" + builder.get_architecture() + ".build" + build_log = os.path.join(buildresult, build_name) + print "Please check %s %s carefully:\nfile://%s\nfile://%s\n" \ + "file://%s" % (task.package, new_version, + debdiff_filename, lintian_filename, + build_log) + answer = yes_edit_no_question("Do you want to upload the " \ + "package to the official " \ + "Ubuntu archive", "yes") + if answer == "edit": + continue + elif answer == "no": + print "Abort." + sys.exit(USER_ABORT) + cmd = ["dput", "--force", upload, changes_file] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error("Upload of %s to %s failed." % \ + (os.path.basename(changes_file), upload)) + sys.exit(1) + if branch: + cmd = ['debcommit'] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error('Bzr commit failed.') + sys.exit(1) + cmd = ['bzr', 'mark-uploaded'] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error('Bzr tagging failed.') + sys.exit(1) + cmd = ['bzr', 'push', ':parent'] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error('Bzr push failed.') + sys.exit(1) + + # Leave while loop if everything worked + break