diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 09:36:25 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 09:36:25 +0000 |
commit | 6077d258b500b20e1e705f5cda567400240c7804 (patch) | |
tree | a5d41c050bd69f91476994b0d30c0a8f1e7957b5 /scripts/debdiff-apply | |
parent | Initial commit. (diff) | |
download | devscripts-6077d258b500b20e1e705f5cda567400240c7804.tar.xz devscripts-6077d258b500b20e1e705f5cda567400240c7804.zip |
Adding upstream version 2.21.3+deb11u1.upstream/2.21.3+deb11u1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'scripts/debdiff-apply')
-rwxr-xr-x | scripts/debdiff-apply | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/scripts/debdiff-apply b/scripts/debdiff-apply new file mode 100755 index 0000000..5875417 --- /dev/null +++ b/scripts/debdiff-apply @@ -0,0 +1,332 @@ +#!/usr/bin/python3 +# Copyright (c) 2016-2017, Ximin Luo <infinity0@debian.org> +# +# 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; either version 3 +# of the License, or (at your option) any later version. +# +# 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-3 for more details. +# +""" +Apply a debdiff to a Debian source package. + +It handles d/changelog hunks specially, to avoid conflicts. + +Depends on dpkg-dev, devscripts, python3-unidiff, quilt. +""" + +import argparse +import email.utils +import hashlib +import logging +import os +import shutil +import subprocess +import sys +import tempfile +import time + +import unidiff + +from debian.changelog import Changelog, ChangeBlock + +# this can be any valid value, it doesn't appear in the final output +DCH_DUMMY_TAIL = "\n -- debdiff-apply dummy tool <infinity0@debian.org> " \ + "Thu, 01 Jan 1970 00:00:00 +0000\n\n" +CHBLOCK_DUMMY_PACKAGE = "debdiff-apply PLACEHOLDER" +TRY_ENCODINGS = ["utf-8", "latin-1"] +DISTRIBUTION_DEFAULT = "experimental" + + +def workaround_dpkg_865430(dscfile, origdir, stdout): + filename = subprocess.check_output( + ["dcmd", "--tar", "echo", dscfile]).rstrip() + if not os.path.exists(os.path.join(origdir.encode("utf-8"), os.path.basename(filename))): + subprocess.check_call( + ["dcmd", "--tar", "cp", dscfile, origdir], stdout=stdout) + + +def is_dch(path): + dirname = os.path.dirname(path) + return (os.path.basename(path) == 'changelog' + and os.path.basename(dirname) == 'debian' + and os.path.dirname(os.path.dirname(dirname)) == '') + + +def hunk_lines_to_str(hunk_lines): + return "".join(map(lambda x: str(x)[1:], hunk_lines)) + + +def read_dch_patch(dch_patch): + if len(dch_patch) > 1: + raise ValueError("don't know how to deal with debian/changelog patch " + "that has more than one hunk") + hunk = dch_patch[0] + source_str = hunk_lines_to_str(hunk.source_lines()) + DCH_DUMMY_TAIL + target_str = hunk_lines_to_str(hunk.target_lines()) + # here we assume the debdiff has enough context to see the previous version + # this should be true all the time in practice + source_version = str(Changelog(source_str, 1)[0].version) + target = Changelog(target_str, 1)[0] + return source_version, target + + +def apply_dch_patch(source_file, current, old_version, target, dry_run): + target_version = str(target.version) + + if not old_version or not target_version.startswith(old_version): + logging.warning("don't know how to rebase version-change (%s => %s) onto %s", + old_version, target_version, old_version) + newlog = subprocess.getoutput("EDITOR=cat dch -n 2>/dev/null").rstrip() + version = str(Changelog(newlog, 1)[0].version) + logging.warning("using version %s based on `dch -n`; feel free to make me smarter", + version) + else: + version_suffix = target_version[len(old_version):] + version = str(current[0].version) + version_suffix + logging.info("using version %s based on suffix %s", + version, version_suffix) + + if dry_run: + return version + + current._blocks.insert(0, target) # pylint: disable=protected-access + current.set_version(version) + + shutil.copy(source_file, source_file + ".new") + try: + with open(source_file + ".new", "w") as fp: + current.write_to_open_file(fp) + os.rename(source_file + ".new", source_file) + except Exception: + logging.warning("failed to patch %s", source_file) + logging.warning("half-applied changes in %s", source_file + ".new") + logging.warning("current working directory is %s", os.getcwd()) + raise + return version + + +def call_patch(patch_str, *args, check=True, **kwargs): + return subprocess.run( + ["patch", "-p1"] + list(args), + input=patch_str, + universal_newlines=True, + check=check, + **kwargs) + + +def check_patch(patch_str, *args, **kwargs): + return call_patch(patch_str, + "--dry-run", "-f", "--silent", + *args, + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + **kwargs).returncode == 0 + + +def debdiff_apply(patch, patch_name, args): + # don't change anything if... + dry_run = args.target_version or args.source_version + + changelog = list(filter(lambda x: is_dch(x.path), patch)) + if not changelog: + logging.info("no debian/changelog in patch: %s", args.patch_file) + old_version = None + target = ChangeBlock( + package=CHBLOCK_DUMMY_PACKAGE, + author="%s <%s>" % (os.getenv("DEBFULLNAME"), + os.getenv("DEBEMAIL")), + date=email.utils.formatdate(time.time(), localtime=True), + version=None, + distributions=args.distribution, + urgency="low", + changes=["", " * Rebase patch %s." % patch_name, ""], + ) + target.add_trailing_line("") + elif len(changelog) > 1: + raise ValueError("more than one debian/changelog patch???") + else: + patch.remove(changelog[0]) + old_version, target = read_dch_patch(changelog[0]) + + if args.source_version: + if old_version: + print(old_version) + return False + + # read this here so --source-version can work even without a d/changelog + with open(args.changelog) as fp: + current = Changelog(fp.read()) + if target.package == CHBLOCK_DUMMY_PACKAGE: + target.package = current[0].package + + if not dry_run: + patch_str = str(patch) + if check_patch(patch_str, "-N"): + call_patch(patch_str) + logging.info("patch %s applies!", patch_name) + elif check_patch(patch_str, "-R"): + logging.warning("patch %s already applied", patch_name) + return False + else: + call_patch(patch_str, "--dry-run", "-f") + raise ValueError("patch %s doesn't apply!" % (patch_name)) + + # only apply d/changelog patch if the rest of the patch applied + new_version = apply_dch_patch( + args.changelog, current, old_version, target, dry_run) + if args.target_version: + print(new_version) + return False + + if args.repl: + import code # pylint: disable=import-outside-toplevel + code.interact(local=locals()) + + return True + + +def parse_args(args): + parser = argparse.ArgumentParser( + description='Apply a debdiff to a Debian source package') + parser.add_argument( + '-v', '--verbose', action="store_true", + help='Output more information', + ) + parser.add_argument( + '-c', '--changelog', default='debian/changelog', + help='Path to debian/changelog; default: %(default)s', + ) + parser.add_argument( + '-D', '--distribution', default='experimental', + help='Distribution to use, if the patch doesn\'t already ' + 'contain a changelog; default: %(default)s', + ) + parser.add_argument( + '--repl', action="store_true", + help="Run the python REPL after processing.", + ) + parser.add_argument( + '--source-version', action="store_true", + help='Don\'t apply the patch; instead print out the version of the ' + 'package that it is supposed to be applied to, or nothing if ' + 'the patch does not specify a source version.', + ) + parser.add_argument( + '--target-version', action="store_true", + help="Don't apply the patch; instead print out the new version of the " + "package debdiff-apply(1) would generate, when the patch is applied to the " + "the given target package, as specified by the other arguments.", + ) + parser.add_argument( + 'orig_dsc_or_dir', nargs='?', default=".", + help="Target to apply the patch to. This can either be an unpacked " + "source tree, or a .dsc file. In the former case, the directory is " + "modified in-place; in the latter case, a second .dsc is created. " + "Default: %(default)s", + ) + parser.add_argument( + 'patch_file', nargs='?', default="/dev/stdin", + help="Patch file to apply, in the format output by debdiff(1). " + "Default: %(default)s", + ) + group1 = parser.add_argument_group('Options for .dsc patch targets') + group1.add_argument( + '--no-clean', action="store_true", + help="Don't clean temporary directories after a failure, so you can " + "examine what failed.", + ) + group1.add_argument( + '--quilt-refresh', action="store_true", + help="If the building of the new source package fails, try to refresh " + "patches using quilt(1) then try building it again.", + ) + group1.add_argument( + '-d', '--directory', default=None, + help="Extract the .dsc into this directory, which won't be cleaned up " + "after debdiff-apply(1) exits. If not given, then it will be extracted to a " + "temporary directory.", + ) + return parser.parse_args(args) + + +def main(args): + # Split this function! pylint: disable=too-many-branches,too-many-locals,too-many-statements + args = parse_args(args) + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + with open(args.patch_file, 'rb') as fp: + data = fp.read() + for enc in TRY_ENCODINGS: + try: + patch = unidiff.PatchSet( + data.splitlines(keepends=True), encoding=enc) + break + except Exception: # pylint: disable=broad-except + if enc == TRY_ENCODINGS[-1]: + raise + continue + + patch_name = '%s:%s' % ( + os.path.basename(args.patch_file), + hashlib.sha256(data).hexdigest()[:20 if args.patch_file == '/dev/stdin' else 8]) + quiet = args.source_version or args.target_version + dry_run = args.source_version or args.target_version + # user can redirect stderr themselves + stdout = subprocess.DEVNULL if quiet else None + + # change directory before applying patches + if os.path.isdir(args.orig_dsc_or_dir): + os.chdir(args.orig_dsc_or_dir) + debdiff_apply(patch, patch_name, args) + elif os.path.isfile(args.orig_dsc_or_dir): + dscfile = args.orig_dsc_or_dir + parts = os.path.splitext(os.path.basename(dscfile)) + if parts[1] != ".dsc": + raise ValueError("unrecognised patch target: %s" % dscfile) + extractdir = args.directory if args.directory else tempfile.mkdtemp() + if not os.path.isdir(extractdir): + os.makedirs(extractdir) + try: + # dpkg-source doesn't like existing dirs + builddir = os.path.join(extractdir, parts[0]) + subprocess.check_call(["dpkg-source", "-x", "--skip-patches", dscfile, builddir], + stdout=stdout) + origdir = os.getcwd() + workaround_dpkg_865430(dscfile, origdir, stdout) + os.chdir(builddir) + did_patch = debdiff_apply(patch, patch_name, args) + if dry_run or not did_patch: + return + os.chdir(origdir) + try: + subprocess.check_call(["dpkg-source", "-b", builddir]) + except subprocess.CalledProcessError: + if args.quilt_refresh: + subprocess.check_call(["sh", "-c", """ +set -ex +export QUILT_PATCHES=debian/patches +while quilt push; do quilt refresh; done +"""], cwd=builddir) + subprocess.check_call(["dpkg-source", "-b", builddir]) + else: + raise + finally: + cleandir = builddir if args.directory else extractdir + if args.no_clean: + logging.warning( + "you should clean up temp files in %s", cleandir) + else: + shutil.rmtree(cleandir) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) |