#!/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. import argparse import sys from collections import defaultdict import apt from distro_info import UbuntuDistroInfo from ubuntutools import getLogger from ubuntutools.config import UDTConfig from ubuntutools.lp.lpapicache import Distribution, Launchpad from ubuntutools.lp.udtexceptions import PackageNotFoundException from ubuntutools.question import EditBugReport, YesNoQuestion, confirmation_prompt from ubuntutools.rdepends import RDependsException, query_rdepends Logger = getLogger() class DestinationException(Exception): pass def determine_destinations(source, destination): ubuntu_info = UbuntuDistroInfo() if destination is None: destination = ubuntu_info.lts() if source not in ubuntu_info.all: raise DestinationException(f"Source release {source} does not exist") if destination not in ubuntu_info.all: raise DestinationException(f"Destination release {destination} does not exist") if destination not in ubuntu_info.supported(): raise DestinationException(f"Destination release {destination} is not supported") 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): """Search for possible existing bug reports""" distro = Distribution("ubuntu") srcpkg = distro.getSourcePackage(name=package.getPackageName()) bugs = srcpkg.searchTasks( omit_duplicates=True, search_text="[BPO]", 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([bug_task.bug for bug_task in bugs], key=lambda bug: bug.id): 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] # pylint: disable=pointless-statement 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 += [f"* {pkg}"] for release, relationship in appearences: output += [f" [ ] {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() try: package_spph = archive.getSourcePackage(package, distribution) return package_spph except PackageNotFoundException as e: try: apt_pkg = apt.Cache()[package] except KeyError: Logger.error(str(e)) sys.exit(1) package = apt_pkg.candidate.source_name Logger.info( "Binary package specified, considering its source package instead: %s", package ) return None 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]", ""] for dest in destinations: testing += [f" * {dest.capitalize()}:"] testing += [" [ ] Package builds without modification"] testing += [f" [ ] {binary} installs cleanly and runs" 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 = "[BPO] %(package)s %(version)s to %(destinations)s" % subst body = ( "\n".join( [ "[Impact]", "", " * Justification for backporting the new version to the stable release.", "", "[Scope]", "", " * List the Ubuntu release you will backport from," " and the specific package version.", "", " * List the Ubuntu release(s) you will backport to.", "", "[Other Info]", "", " * Anything else you think is useful to include", "", ] + 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) distro = Distribution("ubuntu") pkgname = package_spph.getPackageName() bug = Launchpad.bugs.createBug( title=subject, description=body, target=distro.getSourcePackage(name=pkgname) ) bug.subscribe(person=Launchpad.people["ubuntu-backporters"]) for dest in destinations: series = distro.getSeries(dest) try: bug.addTask(target=series.getSourcePackage(name=pkgname)) except Exception: # pylint: disable=broad-except break Logger.info("Backport request filed as %s", bug.web_link) def main(): parser = argparse.ArgumentParser(usage="%(prog)s [options] package") parser.add_argument( "-d", "--destination", metavar="DEST", help="Backport to DEST release and necessary " "intermediate releases " "(default: current LTS release)", ) parser.add_argument( "-s", "--source", metavar="SOURCE", help="Backport from SOURCE release (default: current devel release)", ) parser.add_argument( "-l", "--lpinstance", metavar="INSTANCE", default=None, help="Launchpad instance to connect to (default: production).", ) parser.add_argument( "--no-conf", action="store_true", dest="no_conf", default=False, help="Don't read config files or environment variables", ) parser.add_argument("package", help=argparse.SUPPRESS) args = parser.parse_args() config = UDTConfig(args.no_conf) if args.lpinstance is None: args.lpinstance = config.get_value("LPINSTANCE") Launchpad.login(args.lpinstance) if args.source is None: args.source = Distribution("ubuntu").getDevelopmentSeries().name try: destinations = determine_destinations(args.source, args.destination) except DestinationException as e: Logger.error(str(e)) sys.exit(1) disclaimer() package_spph = locate_package(args.package, args.source) check_existing(package_spph) request_backport(package_spph, args.source, destinations) if __name__ == "__main__": main()