#!/usr/bin/python3 # rediff-patches.py name.spec import argparse import collections import logging import os import re import rpm import shutil import subprocess import sys import tempfile RPMBUILD_ISPATCH = (1<<1) def prepare_spec(r, patch_nr, before=False): tempspec = tempfile.NamedTemporaryFile() re_patch = re.compile(r'^%patch(?P\d+)\w*') for line in r.parsed.split('\n'): m = re_patch.match(line) if m: patch_number = int(m.group('patch_number')) if patch_nr == patch_number: if before: tempspec.write(b"exit 0\n# here was patch%d\n" % patch_nr) else: line = re.sub(r'#.*', "", line) tempspec.write(b"%s\nexit 0\n" % line.encode('utf-8')) continue tempspec.write(b"%s\n" % line.encode('utf-8')) tempspec.flush() return tempspec def unpack(spec, appsourcedir, builddir): cmd = [ 'rpmbuild', '-bp', '--define', '_builddir %s' % builddir, '--define', '_specdir %s' % appsourcedir, '--define', '_sourcedir %s' % appsourcedir, '--define', '_enable_debug_packages 0', '--define', '_default_patch_fuzz 2', spec ] logging.debug("running %s" % repr(cmd)) try: res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True, env={'LC_ALL': 'C.UTF-8'}, timeout=600) except subprocess.CalledProcessError as err: logging.error("unpacking exited with status code %d." % err.returncode) logging.error("STDOUT:") if err.stdout: for line in err.stdout.decode('utf-8').split("\n"): logging.error(line) logging.error("STDERR:") if err.stderr: for line in err.stderr.decode('utf-8').split("\n"): logging.error(line) raise else: logging.debug("unpacking exited with status code %d." % res.returncode) logging.debug("STDOUT/STDERR:") if res.stdout: for line in res.stdout.decode('utf-8').split("\n"): logging.debug(line) def patch_comment_get(patch): patch_comment = "" patch_got = False with open(patch, 'rt') as f: for line in f: if line.startswith('diff ') or line.startswith('--- '): patch_got = True break patch_comment += line return patch_comment if patch_got else "" def diff(diffdir_org, diffdir, builddir, patch_comment, output): diffdir_org = os.path.basename(diffdir_org) diffdir = os.path.basename(diffdir) with open(output, 'wt') as f: if patch_comment: f.write(patch_comment) f.flush() cmd = [ 'diff', '-urNp', '-x', '*.orig', diffdir_org, diffdir ] logging.debug("running %s" % repr(cmd)) try: subprocess.check_call(cmd, cwd=builddir, stdout=f, stderr=sys.stderr, env={'LC_ALL': 'C.UTF-8'}, timeout=600) except subprocess.CalledProcessError as err: if err.returncode != 1: raise logging.info("rediff generated as %s" % output) def diffstat(patch): cmd = [ 'diffstat', patch ] logging.info("running diffstat for: %s" % patch) try: subprocess.check_call(cmd, stdout=sys.stdout, stderr=sys.stderr, env={'LC_ALL': 'C.UTF-8'}, timeout=60) except subprocess.CalledProcessError as err: logging.error("running diffstat failed: %s" % err) except FileNotFoundError as err: logging.error("running diffstat failed: %s, install diffstat package?" % err) def main(): parser = parser = argparse.ArgumentParser(description='rediff patches to avoid fuzzy hunks') parser.add_argument('spec', type=str, help='spec file name') parser.add_argument('-p', '--patches', type=str, help='comma separated list of patch numbers to rediff') parser.add_argument('-s', '--skip-patches', type=str, help='comma separated list of patch numbers to skip rediff') parser.add_argument('-v', '--verbose', help='increase output verbosity', action='store_true') args = parser.parse_args() logging.basicConfig(level=logging.INFO) rpm.setVerbosity(rpm.RPMLOG_ERR) if args.verbose: logging.basicConfig(level=logging.DEBUG) rpm.setVerbosity(rpm.RPMLOG_DEBUG) if args.patches: args.patches = [int(x) for x in args.patches.split(',')] if args.skip_patches: args.skip_patches = [int(x) for x in args.skip_patches.split(',')] specfile = args.spec appsourcedir = os.path.dirname(os.path.abspath(specfile)) try: tempdir = tempfile.TemporaryDirectory(dir="/dev/shm") except FileNotFoundError as e: tempdir = tempfile.TemporaryDirectory(dir="/tmp") topdir = tempdir.name builddir = os.path.join(topdir, 'BUILD') rpm.addMacro("_builddir", builddir) r = rpm.spec(specfile) patches = {} for (name, nr, flags) in r.sources: if flags & RPMBUILD_ISPATCH: patches[nr] = name applied_patches = collections.OrderedDict() re_patch = re.compile(r'^%patch(?P\d+)\w*(?P.*)') for line in r.parsed.split('\n'): m = re_patch.match(line) if not m: continue patch_nr = int(m.group('patch_number')) patch_args = m.group('patch_args') applied_patches[patch_nr] = patch_args appbuilddir = rpm.expandMacro("%{_builddir}/%{?buildsubdir}") for patch_nr in applied_patches.keys(): if args.patches and patch_nr not in args.patches: continue if args.skip_patches and patch_nr in args.skip_patches: continue patch_name = patches[patch_nr] logging.info("*** patch %d: %s" % (patch_nr, patch_name)) tempspec = prepare_spec(r, patch_nr, before=True) unpack(tempspec.name, appsourcedir, builddir) tempspec.close() os.rename(appbuilddir, appbuilddir + ".org") tempspec = prepare_spec(r, patch_nr, before=False) unpack(tempspec.name, appsourcedir, builddir) tempspec.close() patch_comment = patch_comment_get(patch_name) diff(appbuilddir + ".org", appbuilddir, builddir, patch_comment, os.path.join(topdir, os.path.join(appsourcedir, patch_name + ".rediff"))) diffstat(os.path.join(topdir, os.path.join(appsourcedir, patch_name))) diffstat(os.path.join(topdir, os.path.join(appsourcedir, patch_name + ".rediff"))) shutil.rmtree(builddir) tempdir.cleanup() if __name__ == '__main__': main()