#!/usr/bin/python3 # # Copyright (C) 2011, Stefano Rivera # # 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. from collections import defaultdict import optparse import re import sys import apt from distro_info import UbuntuDistroInfo from ubuntutools.config import UDTConfig from ubuntutools.lp.lpapicache import Launchpad, Distribution from ubuntutools.lp.udtexceptions import PackageNotFoundException from ubuntutools.question import (YesNoQuestion, EditBugReport, confirmation_prompt) from ubuntutools.rdepends import query_rdepends, RDependsException from ubuntutools import getLogger Logger = getLogger(__name__) class DestinationException(Exception): pass def determine_destinations(source, destination): ubuntu_info = UbuntuDistroInfo() if destination is None: destination = ubuntu_info.stable() if source not in ubuntu_info.all: raise DestinationException("Source release %s does not exist" % source) if destination not in ubuntu_info.all: raise DestinationException("Destination release %s does not exist" % destination) if destination not in ubuntu_info.supported(): raise DestinationException("Destination release %s is not supported" % destination) found = False destinations = [] support_gap = False for release in ubuntu_info.all: if release == destination: found = True if release == source: break if found: if support_gap: if ubuntu_info.is_lts(release): support_gap = False else: continue if release not in ubuntu_info.supported(): support_gap = True continue destinations.append(release) assert found assert len(destinations) > 0 return destinations def disclaimer(): print("Ubuntu's backports are not for fixing bugs in stable releases, " "but for bringing new features to older, stable releases.\n" "See https://wiki.ubuntu.com/UbuntuBackports for the Ubuntu " "Backports policy and processes.\n" "See https://wiki.ubuntu.com/StableReleaseUpdates for the process " "for fixing bugs in stable releases.") confirmation_prompt() def check_existing(package, destinations): """Search for possible existing bug reports""" # The LP bug search is indexed, not substring: query = re.findall(r'[a-z]+', package) bugs = [] for release in destinations: project_name = '{}-backports'.format(release) try: project = Launchpad.projects[project_name] except KeyError: Logger.error("The backports tracking project '%s' doesn't seem to " "exist. Please check the situation with the " "backports team.", project_name) sys.exit(1) bugs += project.searchTasks(omit_duplicates=True, search_text=query, status=["Incomplete", "New", "Confirmed", "Triaged", "In Progress", "Fix Committed"]) if not bugs: return Logger.info("There are existing bug reports that look similar to your " "request. Please check before continuing:") for bug in sorted(set(bug_task.bug for bug_task in bugs)): Logger.info(" * LP: #%-7i: %s %s", bug.id, bug.title, bug.web_link) confirmation_prompt() def find_rdepends(releases, published_binaries): intermediate = defaultdict(lambda: defaultdict(list)) # We want to display every pubilshed binary, even if it has no rdepends for binpkg in published_binaries: intermediate[binpkg] for arch in ('any', 'source'): for release in releases: for binpkg in published_binaries: try: raw_rdeps = query_rdepends(binpkg, release, arch) except RDependsException: # Not published? TODO: Check continue for relationship, rdeps in raw_rdeps.items(): for rdep in rdeps: # Ignore circular deps: if rdep['Package'] in published_binaries: continue # arch==any queries return Reverse-Build-Deps: if arch == 'any' and rdep.get('Architectures', []) == ['source']: continue intermediate[binpkg][rdep['Package']].append((release, relationship)) output = [] for binpkg, rdeps in intermediate.items(): output += ['', binpkg, '-' * len(binpkg)] for pkg, appearences in rdeps.items(): output += ['* %s' % pkg] for release, relationship in appearences: output += [' [ ] %s (%s)' % (release, relationship)] found_any = sum(len(rdeps) for rdeps in intermediate.values()) if found_any: output = [ "Reverse dependencies:", "=====================", "The following reverse-dependencies need to be tested against the " "new version of %(package)s. " "For reverse-build-dependencies (-Indep), please test that the " "package still builds against the new %(package)s. " "For reverse-dependencies, please test that the version of the " "package currently in the release still works with the new " "%(package)s installed. " "Reverse- Recommends, Suggests, and Enhances don't need to be " "tested, and are listed for completeness-sake." ] + output else: output = ["No reverse dependencies"] return output def locate_package(package, distribution): archive = Distribution('ubuntu').getArchive() for pass_ in ('source', 'binary'): try: package_spph = archive.getSourcePackage(package, distribution) return package_spph except PackageNotFoundException as e: if pass_ == 'binary': Logger.error(str(e)) sys.exit(1) try: apt_pkg = apt.Cache()[package] except KeyError: continue package = apt_pkg.candidate.source_name Logger.info("Binary package specified, considering its source " "package instead: %s", package) def request_backport(package_spph, source, destinations): published_binaries = set() for bpph in package_spph.getBinaries(): published_binaries.add(bpph.getPackageName()) if not published_binaries: Logger.error("%s (%s) has no published binaries in %s. ", package_spph.getPackageName(), package_spph.getVersion(), source) Logger.info("Is it stuck in bin-NEW? It can't be backported until " "the binaries have been accepted.") sys.exit(1) testing = [] testing += ["You can test-build the backport in your PPA with " "backportpackage:"] testing += ["$ backportpackage -u ppa:/ " "-s %s -d %s %s" % (source, dest, package_spph.getPackageName()) for dest in destinations] testing += [""] for dest in destinations: testing += ['* %s:' % dest] testing += ["[ ] Package builds without modification"] testing += ["[ ] %s installs cleanly and runs" % binary for binary in published_binaries] subst = { 'package': package_spph.getPackageName(), 'version': package_spph.getVersion(), 'component': package_spph.getComponent(), 'source': package_spph.getSeriesAndPocket(), 'destinations': ', '.join(destinations), } subject = ("Please backport %(package)s %(version)s (%(component)s) " "from %(source)s" % subst) body = ('\n'.join( [ "Please backport %(package)s %(version)s (%(component)s) " "from %(source)s to %(destinations)s.", "", "Reason for the backport:", "========================", ">>> Enter your reasoning here <<<", "", "Testing:", "========", "Mark off items in the checklist [X] as you test them, " "but please leave the checklist so that backporters can quickly " "evaluate the state of testing.", "" ] + testing + [""] + find_rdepends(destinations, published_binaries) + [""]) % subst) editor = EditBugReport(subject, body) editor.edit() subject, body = editor.get_report() Logger.info('The final report is:\nSummary: %s\nDescription:\n%s\n', subject, body) if YesNoQuestion().ask("Request this backport", "yes") == "no": sys.exit(1) targets = [Launchpad.projects['%s-backports' % destination] for destination in destinations] bug = Launchpad.bugs.createBug(title=subject, description=body, target=targets[0]) for target in targets[1:]: bug.addTask(target=target) Logger.info("Backport request filed as %s", bug.web_link) def main(): parser = optparse.OptionParser('%prog [options] package') parser.add_option('-d', '--destination', metavar='DEST', help='Backport to DEST release and necessary ' 'intermediate releases ' '(default: current stable release)') parser.add_option('-s', '--source', metavar='SOURCE', help='Backport from SOURCE release ' '(default: current devel release)') parser.add_option('-l', '--lpinstance', metavar='INSTANCE', default=None, help='Launchpad instance to connect to ' '(default: production).') parser.add_option('--no-conf', action='store_true', dest='no_conf', default=False, help="Don't read config files or environment variables") options, args = parser.parse_args() if len(args) != 1: parser.error("One (and only one) package must be specified") package = args[0] config = UDTConfig(options.no_conf) if options.lpinstance is None: options.lpinstance = config.get_value('LPINSTANCE') Launchpad.login(options.lpinstance) if options.source is None: options.source = Distribution('ubuntu').getDevelopmentSeries().name try: destinations = determine_destinations(options.source, options.destination) except DestinationException as e: Logger.error(str(e)) sys.exit(1) disclaimer() check_existing(package, destinations) package_spph = locate_package(package, options.source) request_backport(package_spph, options.source, destinations) if __name__ == '__main__': main()