ubuntu-dev-tools/requestbackport
Benjamin Drung aa556af89d Use f-strings
pylint complains about C0209: Formatting a regular string which could be
a f-string (consider-using-f-string)

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 19:32:58 +01:00

327 lines
11 KiB
Python
Executable File

#!/usr/bin/python3
#
# Copyright (C) 2011, Stefano Rivera <stefanor@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 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()