]>
Commit | Line | Data |
---|---|---|
7eacbddd AM |
1 | #!/usr/bin/python3 |
2 | ||
3 | # rediff-patches.py name.spec | |
4 | ||
1e57e977 AM |
5 | import argparse |
6 | import collections | |
7 | import logging | |
7eacbddd AM |
8 | import os |
9 | import re | |
10 | import rpm | |
11 | import shutil | |
12 | import subprocess | |
13 | import sys | |
14 | import tempfile | |
15 | ||
16 | RPMBUILD_ISPATCH = (1<<1) | |
17 | ||
ce75a2bd AM |
18 | def prepare_spec(r, patch_nr, before=False): |
19 | tempspec = tempfile.NamedTemporaryFile() | |
20 | re_patch = re.compile(r'^%patch(?P<patch_number>\d+)\w*') | |
21 | for line in r.parsed.split('\n'): | |
22 | m = re_patch.match(line) | |
23 | if m: | |
24 | patch_number = int(m.group('patch_number')) | |
25 | if patch_nr == patch_number: | |
26 | if before: | |
27 | tempspec.write(b"exit 0\n# here was patch%d\n" % patch_nr) | |
28 | else: | |
29 | line = re.sub(r'#.*', "", line) | |
30 | tempspec.write(b"%s\nexit 0\n" % line.encode('utf-8')) | |
31 | continue | |
32 | tempspec.write(b"%s\n" % line.encode('utf-8')) | |
33 | tempspec.flush() | |
34 | return tempspec | |
35 | ||
98607168 | 36 | def unpack(spec, appsourcedir, builddir): |
ce75a2bd AM |
37 | cmd = [ 'rpmbuild', '-bp', |
38 | '--define', '_builddir %s' % builddir, | |
98607168 AM |
39 | '--define', '_specdir %s' % appsourcedir, |
40 | '--define', '_sourcedir %s' % appsourcedir, | |
ce75a2bd AM |
41 | '--define', '_enable_debug_packages 0', |
42 | '--define', '_default_patch_fuzz 2', | |
43 | spec ] | |
1e57e977 | 44 | logging.debug("running %s" % repr(cmd)) |
98607168 AM |
45 | try: |
46 | res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True, | |
47 | env={'LC_ALL': 'C.UTF-8'}, timeout=600) | |
48 | except subprocess.CalledProcessError as err: | |
49 | logging.error("unpacking exited with status code %d." % err.returncode) | |
50 | logging.error("STDOUT:") | |
51 | if err.stdout: | |
52 | for line in err.stdout.decode('utf-8').split("\n"): | |
53 | logging.error(line) | |
54 | logging.error("STDERR:") | |
55 | if err.stderr: | |
56 | for line in err.stderr.decode('utf-8').split("\n"): | |
57 | logging.error(line) | |
58 | raise | |
59 | else: | |
60 | logging.debug("unpacking exited with status code %d." % res.returncode) | |
61 | logging.debug("STDOUT/STDERR:") | |
62 | if res.stdout: | |
63 | for line in res.stdout.decode('utf-8').split("\n"): | |
64 | logging.debug(line) | |
65 | ||
7eacbddd | 66 | |
3cc64e89 AM |
67 | def patch_comment_get(patch): |
68 | patch_comment = "" | |
4c36d189 | 69 | patch_got = False |
3cc64e89 AM |
70 | with open(patch, 'rt') as f: |
71 | for line in f: | |
72 | if line.startswith('diff ') or line.startswith('--- '): | |
4c36d189 | 73 | patch_got = True |
3cc64e89 AM |
74 | break |
75 | patch_comment += line | |
4c36d189 | 76 | return patch_comment if patch_got else "" |
3cc64e89 AM |
77 | |
78 | def diff(diffdir_org, diffdir, builddir, patch_comment, output): | |
7eacbddd AM |
79 | diffdir_org = os.path.basename(diffdir_org) |
80 | diffdir = os.path.basename(diffdir) | |
81 | ||
82 | with open(output, 'wt') as f: | |
3cc64e89 AM |
83 | if patch_comment: |
84 | f.write(patch_comment) | |
85 | f.flush() | |
7eacbddd | 86 | cmd = [ 'diff', '-urNp', '-x', '*.orig', diffdir_org, diffdir ] |
1e57e977 | 87 | logging.debug("running %s" % repr(cmd)) |
7eacbddd | 88 | try: |
1e57e977 AM |
89 | subprocess.check_call(cmd, cwd=builddir, stdout=f, stderr=sys.stderr, |
90 | env={'LC_ALL': 'C.UTF-8'}, timeout=600) | |
7eacbddd AM |
91 | except subprocess.CalledProcessError as err: |
92 | if err.returncode != 1: | |
93 | raise | |
1e57e977 AM |
94 | logging.info("rediff generated as %s" % output) |
95 | ||
0f726ca6 AM |
96 | def diffstat(patch): |
97 | cmd = [ 'diffstat', patch ] | |
98 | logging.info("running diffstat for: %s" % patch) | |
99 | try: | |
100 | subprocess.check_call(cmd, stdout=sys.stdout, stderr=sys.stderr, | |
101 | env={'LC_ALL': 'C.UTF-8'}, timeout=60) | |
102 | except subprocess.CalledProcessError as err: | |
103 | logging.error("running diffstat failed: %s" % err) | |
104 | except FileNotFoundError as err: | |
105 | logging.error("running diffstat failed: %s, install diffstat package?" % err) | |
106 | ||
c65b587f | 107 | def main(): |
1e57e977 AM |
108 | parser = parser = argparse.ArgumentParser(description='rediff patches to avoid fuzzy hunks') |
109 | parser.add_argument('spec', type=str, help='spec file name') | |
110 | parser.add_argument('-p', '--patches', type=str, help='comma separated list of patch numbers to rediff') | |
1cae170b | 111 | parser.add_argument('-s', '--skip-patches', type=str, help='comma separated list of patch numbers to skip rediff') |
1e57e977 AM |
112 | parser.add_argument('-v', '--verbose', help='increase output verbosity', action='store_true') |
113 | args = parser.parse_args() | |
114 | ||
115 | logging.basicConfig(level=logging.INFO) | |
116 | rpm.setVerbosity(rpm.RPMLOG_ERR) | |
117 | ||
118 | if args.verbose: | |
119 | logging.basicConfig(level=logging.DEBUG) | |
120 | rpm.setVerbosity(rpm.RPMLOG_DEBUG) | |
121 | ||
122 | if args.patches: | |
123 | args.patches = [int(x) for x in args.patches.split(',')] | |
124 | ||
1cae170b AM |
125 | if args.skip_patches: |
126 | args.skip_patches = [int(x) for x in args.skip_patches.split(',')] | |
127 | ||
1e57e977 | 128 | specfile = args.spec |
98607168 | 129 | appsourcedir = os.path.dirname(os.path.abspath(specfile)) |
c65b587f | 130 | |
43ad78f8 AM |
131 | try: |
132 | tempdir = tempfile.TemporaryDirectory(dir="/dev/shm") | |
133 | except FileNotFoundError as e: | |
134 | tempdir = tempfile.TemporaryDirectory(dir="/tmp") | |
c65b587f AM |
135 | topdir = tempdir.name |
136 | builddir = os.path.join(topdir, 'BUILD') | |
137 | ||
138 | rpm.addMacro("_builddir", builddir) | |
139 | ||
140 | r = rpm.spec(specfile) | |
141 | ||
142 | patches = {} | |
143 | ||
144 | for (name, nr, flags) in r.sources: | |
145 | if flags & RPMBUILD_ISPATCH: | |
146 | patches[nr] = name | |
147 | ||
1e57e977 | 148 | applied_patches = collections.OrderedDict() |
c65b587f AM |
149 | re_patch = re.compile(r'^%patch(?P<patch_number>\d+)\w*(?P<patch_args>.*)') |
150 | for line in r.parsed.split('\n'): | |
151 | m = re_patch.match(line) | |
152 | if not m: | |
153 | continue | |
154 | patch_nr = int(m.group('patch_number')) | |
155 | patch_args = m.group('patch_args') | |
156 | applied_patches[patch_nr] = patch_args | |
157 | ||
c65b587f AM |
158 | appbuilddir = rpm.expandMacro("%{_builddir}/%{?buildsubdir}") |
159 | ||
ce75a2bd | 160 | for patch_nr in applied_patches.keys(): |
1e57e977 | 161 | if args.patches and patch_nr not in args.patches: |
c65b587f | 162 | continue |
1cae170b AM |
163 | if args.skip_patches and patch_nr in args.skip_patches: |
164 | continue | |
1e57e977 AM |
165 | patch_name = patches[patch_nr] |
166 | logging.info("*** patch %d: %s" % (patch_nr, patch_name)) | |
ce75a2bd AM |
167 | |
168 | tempspec = prepare_spec(r, patch_nr, before=True) | |
98607168 | 169 | unpack(tempspec.name, appsourcedir, builddir) |
ce75a2bd | 170 | tempspec.close() |
c65b587f | 171 | os.rename(appbuilddir, appbuilddir + ".org") |
ce75a2bd AM |
172 | |
173 | tempspec = prepare_spec(r, patch_nr, before=False) | |
98607168 | 174 | unpack(tempspec.name, appsourcedir, builddir) |
ce75a2bd AM |
175 | tempspec.close() |
176 | ||
3cc64e89 AM |
177 | patch_comment = patch_comment_get(patch_name) |
178 | diff(appbuilddir + ".org", | |
179 | appbuilddir, | |
180 | builddir, | |
181 | patch_comment, | |
182 | os.path.join(topdir, os.path.join(appsourcedir, patch_name + ".rediff"))) | |
ce75a2bd | 183 | |
0f726ca6 AM |
184 | diffstat(os.path.join(topdir, os.path.join(appsourcedir, patch_name))) |
185 | diffstat(os.path.join(topdir, os.path.join(appsourcedir, patch_name + ".rediff"))) | |
186 | ||
c65b587f AM |
187 | shutil.rmtree(builddir) |
188 | tempdir.cleanup() | |
189 | ||
190 | if __name__ == '__main__': | |
191 | main() |