update-patches 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. #! /usr/bin/perl
  2. # $OpenBSD: update-patches,v 1.16 2017/05/25 23:15:41 espie Exp $
  3. # Copyright (c) 2017
  4. # Marc Espie. All rights reserved.
  5. # Redistribution and use in source and binary forms, with or without
  6. # modification, are permitted provided that the following conditions
  7. # are met:
  8. # 1. Redistributions of code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. # 2. Neither the name of OpenBSD nor the names of its contributors
  11. # may be used to endorse or promote products derived from this software
  12. # without specific prior written permission.
  13. #
  14. # THIS SOFTWARE IS PROVIDED BY ITS AUTHOR AND THE OpenBSD project ``AS IS'' AND
  15. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  16. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  17. # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
  18. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  19. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  20. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  21. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  22. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  23. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  24. # SUCH DAMAGE.
  25. use File::Find;
  26. use strict;
  27. use warnings;
  28. use feature qw(say);
  29. # our "normal" output is STDERR
  30. open my $oldout, '>&STDOUT';
  31. open STDOUT, '>&STDERR';
  32. # grab env stuff
  33. my ($distorig, $patchorig, $wrkdist, $patchdir, $patch_list) =
  34. ($ENV{DISTORIG}, $ENV{PATCHORIG}, $ENV{WRKDIST}, $ENV{PATCHDIR},
  35. $ENV{PATCH_LIST});
  36. if ($patchorig ne '.orig') {
  37. say "PATCHORIG=$patchorig";
  38. }
  39. my $force = defined($ENV{FORCE_REGEN});
  40. my $verbose = defined($ENV{PATCH_VERBOSE});
  41. # protect against dirty stuff
  42. $wrkdist =~ s/\/$//g;
  43. my @diff_args;
  44. # XXX more processing maybe ?
  45. if (defined $ENV{DIFF_ARGS}) {
  46. push(@diff_args, split(/\s+/, $ENV{DIFF_ARGS}));
  47. }
  48. if (!-d $wrkdist) {
  49. say "WRKDIST=$wrkdist is not a directory";
  50. exit 1;
  51. }
  52. my ($actual, $saved, $done, $nochange);
  53. my @edit;
  54. my $kw_re = qr{\$(
  55. Author|CVSHeader|Date|Header|Id|Name|Locker|Log|
  56. RCSFile|Revision|Source|State|OpenBSD
  57. )\b.*\$}x;
  58. sub fuzz_chunk
  59. {
  60. my $chunk = shift;
  61. return 0 if @{$chunk->{lines}} < 4;
  62. my $zap = 0;
  63. my $fuzzed = 0;
  64. if ($chunk->{lines}[0] =~ m/^\s/ &&
  65. $chunk->{lines}[0] =~ m/$kw_re/) {
  66. $zap = 1;
  67. }
  68. if ($chunk->{lines}[0] =~ m/^\s/ &&
  69. $chunk->{lines}[1] =~ m/^\s/ &&
  70. $chunk->{lines}[1] =~ m/$kw_re/) {
  71. $zap = 2;
  72. }
  73. while ($zap) {
  74. shift @{$chunk->{lines}};
  75. $chunk->{oldstart}++;
  76. $chunk->{newstart}++;
  77. $chunk->{oldplus}--;
  78. $chunk->{newplus}--;
  79. $zap--;
  80. $fuzzed = 1;
  81. }
  82. if ($chunk->{lines}[-1] =~ m/^\s/ &&
  83. $chunk->{lines}[-1] =~ m/$kw_re/) {
  84. $zap=1;
  85. }
  86. if ($chunk->{lines}[-1] =~ m/^\s/ &&
  87. $chunk->{lines}[-2] =~ m/^\s/ &&
  88. $chunk->{lines}[-2] =~ m/$kw_re/) {
  89. $zap=2;
  90. }
  91. while ($zap) {
  92. pop @{$chunk->{lines}};
  93. $chunk->{oldplus}--;
  94. $chunk->{newplus}--;
  95. $zap--;
  96. $fuzzed = 1;
  97. }
  98. return $fuzzed;
  99. }
  100. sub may_fuzz_patch
  101. {
  102. my ($stem, $list) = @_;
  103. my $try_fuzz = 0;
  104. for my $l (@$list) {
  105. if ($l =~ m/$kw_re/) {
  106. $try_fuzz = 1;
  107. last;
  108. }
  109. }
  110. return unless $try_fuzz;
  111. my @lines = @$list;
  112. if (@lines < 2) {
  113. return;
  114. }
  115. # extract the header
  116. my $h1 = shift @lines;
  117. my $h2 = shift @lines;
  118. # cut up the patch
  119. my $patch = [];
  120. my $chunk;
  121. my $fuzzed = 0;
  122. while (@lines > 0) {
  123. my $l = shift @lines;
  124. if ($l =~ m/^\@\@\s+\-(\d+)\,(\d+)\s+\+(\d+)\,(\d+)\s+\@\@$/) {
  125. if (defined $chunk) {
  126. if ($chunk->{fuzzable} && fuzz_chunk($chunk)) {
  127. $fuzzed = 1;
  128. }
  129. push(@$patch, $chunk);
  130. }
  131. $chunk = {oldstart => $1, oldplus => $2,
  132. newstart => $3, newplus => $4};
  133. } else {
  134. return if !defined $chunk;
  135. if ($l =~ m/$kw_re/) {
  136. $chunk->{fuzzable} = 1;
  137. }
  138. push(@{$chunk->{lines}}, $l);
  139. }
  140. }
  141. if (defined $chunk) {
  142. if ($chunk->{fuzzable} && fuzz_chunk($chunk)) {
  143. $fuzzed = 1;
  144. }
  145. push(@$patch, $chunk);
  146. }
  147. return unless $fuzzed;
  148. say "*** Patch for $stem fuzzed because of CVS keywords" if $verbose;
  149. @$list = ($h1, $h2);
  150. for my $chunk (@{$patch}) {
  151. push(@$list, '@@ -'.$chunk->{oldstart}.','.$chunk->{oldplus}.
  152. ' +'.$chunk->{newstart}.','.$chunk->{newplus}.
  153. ' @@'."\n");
  154. push(@$list, @{$chunk->{lines}});
  155. }
  156. }
  157. sub create_patch
  158. {
  159. my ($src, $dst, $stem) = @_;
  160. say "Processing $stem" if $verbose;
  161. open(my $file, "-|", "diff", "-u", "-p", "-a", @diff_args, "-L",
  162. "$stem.orig", "-L", $stem, "--", $src, $dst) or die;
  163. my @lines = <$file>;
  164. close $file;
  165. my $comment = "!OpenBSD!\n";
  166. $comment =~ tr/!/$/;
  167. may_fuzz_patch($stem, \@lines);
  168. return {stem => $stem, patch => \@lines,
  169. filename => patch_name($stem),
  170. comment => [$comment, "\n"] };
  171. }
  172. sub parse_existing_patch
  173. {
  174. my $filename = shift;
  175. open (my $f, '<', $filename) or die;
  176. my (@comment, $src, @patch);
  177. while (<$f>) {
  178. if (m/^Index:\s+(\S.*)/) {
  179. $src = $1;
  180. while (<$f>) {
  181. push(@patch, $_);
  182. }
  183. last;
  184. }
  185. # XXX have to do *two* matches so that $1 is okay
  186. # otherwise if $patchorig = 'sthg.orig' this will fail
  187. if (m/^\-\-\-\s+(\S.*)\Q$patchorig\E/ ||
  188. m/^\-\-\-\s+(\S.*)\.orig/) {
  189. push(@patch, $_);
  190. $src = $1;
  191. while (<$f>) {
  192. push(@patch, $_);
  193. }
  194. last;
  195. }
  196. push(@comment, $_);
  197. }
  198. return {stem => $src, filename => $filename,
  199. comment => \@comment, patch => \@patch};
  200. }
  201. sub write_patch
  202. {
  203. my $p = shift;
  204. if (-f $p->{filename}) {
  205. rename $p->{filename}, $p->{filename}.".orig" or die;
  206. }
  207. open(my $f, '>', $p->{filename}) or die;
  208. for my $l (@{$p->{comment}}) {
  209. print $f $l;
  210. }
  211. if (defined $p->{stem}) {
  212. print $f "Index: $p->{stem}\n";
  213. }
  214. for my $l (@{$p->{patch}}) {
  215. print $f $l;
  216. }
  217. close $f or die;
  218. }
  219. sub patch_name
  220. {
  221. my $arg = shift;
  222. $arg =~ s/[\s\/\.]/_/g;
  223. return "patch-$arg";
  224. }
  225. sub description
  226. {
  227. my $p = shift;
  228. if ($p->{filename} ne patch_name($p->{stem})) {
  229. return "$p->{filename} for $p->{stem}";
  230. } else {
  231. return "for $p->{stem}";
  232. }
  233. }
  234. sub patches_differ
  235. {
  236. my ($a, $b) = @_;
  237. if (@{$a->{patch}} != @{$b->{patch}}) {
  238. return 1;
  239. }
  240. my @m = @{$b->{patch}};
  241. for my $l (@{$a->{patch}}) {
  242. my $m = shift @m;
  243. next if $l =~ m/^(\-\-\-|\+\+\+)\s+\Q$a->{stem}\E/;
  244. return 1 if $l ne $m;
  245. }
  246. return 0;
  247. }
  248. sub identical_msg
  249. {
  250. my $name = shift;
  251. return "$name and $name$distorig are identical";
  252. }
  253. # figure out which files to work with
  254. find({wanted =>
  255. sub {
  256. return if -l $_;
  257. return unless -f _;
  258. return unless m/\Q$patchorig\E$/;
  259. return if $_ eq 'Oops.rej.orig';
  260. return if m/\Q$distorig\E$/;
  261. # avoid double reporting patches
  262. my $src = $File::Find::name;
  263. my $dst = $src;
  264. $dst =~ s/\Q$patchorig\E$//;
  265. # don't double-report
  266. return if $dst =~ m/^(.*)\.beforesubst$/ && -f $1.$patchorig;
  267. my $stem = $dst;
  268. $stem =~ s/^\Q$wrkdist\E\///;
  269. my $attach = '';
  270. if (-f "$dst.beforesubst") {
  271. $dst = "$dst.beforesubst";
  272. $attach = '.beforesubst';
  273. } elsif (!-f $dst) {
  274. say "$stem not found";
  275. return;
  276. }
  277. require File::Compare;
  278. if (File::Compare::compare($src, $dst) == 0) {
  279. if ($verbose) {
  280. say identical_msg($stem);
  281. } else {
  282. $nochange->{$stem} = 1;
  283. }
  284. return;
  285. }
  286. my $p = create_patch($src, $dst, $stem);
  287. $actual->{$p->{stem}} = $p;
  288. }, follow => 1, follow_skip => 2 }, $wrkdist);
  289. # do we have patches ?
  290. if (keys %$actual) {
  291. unless (-d $patchdir) {
  292. require File::Path;
  293. File::Path::make_path($patchdir) or die;
  294. }
  295. }
  296. if (chdir($patchdir)) {
  297. # figure out which patch is which
  298. for my $i (glob $patch_list) {
  299. next unless -f $i;
  300. next if $i =~ m/(\.orig|\.rej|\~)$/;
  301. $done->{$i} = 1;
  302. my $parsed = parse_existing_patch($i);
  303. if (!defined $parsed->{stem}) {
  304. say "*** File $i is not a proper patch";
  305. $parsed->{stem} = $i;
  306. }
  307. $saved->{$parsed->{stem}} = $parsed;
  308. }
  309. }
  310. # handle patches
  311. for my $k (sort keys %$actual) {
  312. my $p = $actual->{"$k"};
  313. # is there already a patch ? we need to compare
  314. if (exists $saved->{$k}) {
  315. my $o = $saved->{$k};
  316. my $differ = patches_differ($o, $p);
  317. $o->{accounted} = 1;
  318. next unless $differ || $force;
  319. $o->{patch} = $p->{patch};
  320. write_patch($o);
  321. next unless $differ;
  322. say "Patch ", description($o), " updated";
  323. system {"diff"} ('diff', '-u', @diff_args, '--',
  324. $o->{filename}.".orig", $o->{filename}) if $verbose;
  325. push(@edit, $o->{filename});
  326. } else {
  327. say "New patch ", description($p);
  328. write_patch($p);
  329. # register it as known so we don't reparse
  330. $saved->{$p->{stem}} = $p;
  331. $done->{$p->{filename}} = 1;
  332. $p->{accounted} = 1;
  333. push(@edit, $p->{filename});
  334. }
  335. }
  336. # parse supplementary files
  337. if (chdir($patchdir)) {
  338. for my $i (glob '*') {
  339. next unless -f $i;
  340. next if $i =~ m/(\.orig|\.rej|\~)$/;
  341. next if $done->{$i};
  342. my $parsed = parse_existing_patch($i);
  343. $parsed->{stem} //= $i;
  344. $saved->{$parsed->{stem}} = $parsed;
  345. }
  346. }
  347. #for my $k (sort {$a->{filename} cmp $b->{filename}} keys %$old) {
  348. for my $k (sort keys %$saved) {
  349. my $p = $saved->{"$k"};
  350. if (!$p->{accounted}) {
  351. if ($nochange->{$p->{stem}}) {
  352. say identical_msg($p->{stem});
  353. }
  354. say "*** Patch ", description($p), " not accounted for";
  355. }
  356. my ($warned_newline, $warned_keyword) = (0, 0);
  357. for my $l (@{$p->{patch}}) {
  358. if ($l =~ m/^\\ No newline at end of file/) {
  359. say "*** Patch ", description($p), " misses newline at end of file"
  360. unless $warned_newline;
  361. $warned_newline = 1;
  362. } elsif ($l =~ m/$kw_re/) {
  363. say "*** Patch ", description($p), " contains CVS keyword"
  364. unless $warned_keyword;
  365. $warned_keyword = 1;
  366. }
  367. }
  368. }
  369. say $oldout join(' ', @edit);