This is a live mirror of the Perl 5 development currently hosted at https://github.com/perl/perl5
Make Porting/bump-perl-version do multiple substitutions per line
[perl5.git] / Porting / bump-perl-version
1 #!/usr/bin/perl
2 #
3 # bump-perl-version, DAPM 14 Jul 2009
4 #
5 # A utility to find, and optionally bump, references to the perl version
6 # number in various files within the perl source
7 #
8 # It's designed to work in two phases. First, when run with -s (scan),
9 # it searches all the files in MANIFEST looking for strings that appear to
10 # match the current perl version (or which it knows are *supposed* to
11 # contain the current version), and produces a list of them to stdout,
12 # along with a suggested edit. For example:
13 #
14 #     $ Porting/bump-perl-version -s 5.10.0 5.10.1 > /tmp/scan
15 #     $ cat /tmp/scan
16 #     Porting/config.sh
17 #     
18 #     52: -archlib='/opt/perl/lib/5.10.0/i686-linux-64int'
19 #         +archlib='/opt/perl/lib/5.10.1/i686-linux-64int'
20 #     ....
21 #
22 # At this point there will be false positives. Edit the file to remove
23 # those changes you don't want made. Then in the second phase, feed that
24 # list in, and it will change those lines in the files:
25 #
26 #     $ Porting/bump-perl-version -u < /tmp/scan
27 #
28 # (so line 52 of Porting/config.sh is now updated)
29
30 # This utility 'knows' about certain files and formats, and so can spot
31 # 'hidden' version numbers, like PERL_SUBVERSION=9.
32 #
33 # A third variant makes use of this knowledge to check that all the things
34 # it knows about are at the current version:
35 #
36 #    $ Porting/bump-perl-version -c 5.10.0
37 #
38 # XXX this script hasn't been tested against a major version bump yet,
39 # eg 5.11.0 to 5.12.0; there may be things it missed - DAPM 14 Jul 09
40 #
41 # Note there are various files and directories that it skips; these are
42 # ones that are unlikely to contain anything needing bumping, but which
43 # will generate lots fo false positives (eg pod/*). These are listed on
44 # STDERR as they are skipped.
45
46 use strict;
47 use warnings;
48 use autodie;
49 use Getopt::Std;
50 use ExtUtils::Manifest;
51
52
53 sub usage { die <<EOF }
54
55 @_
56
57 usage: $0 -c <C.C.C>
58           -s <C.C.C> <N.N.N>
59           -u
60           -i <C.C.C> <N.N.N>
61
62     -c check files and warn if any known string values (eg
63         PERL_SUBVERSION) don't match the specified version
64
65     -s scan files and produce list of possible change lines to stdout
66
67     -u read in the scan file from stdin, and change all the lines specified
68
69     -i scan files and make changes inplace
70
71     C.C.C the current perl version, eg 5.10.0
72     N.N.N the new     perl version, eg 5.10.1
73 EOF
74
75 my %opts;
76 getopts('csui', \%opts) or usage;
77 if ($opts{u}) {
78     @ARGV == 0 or usage('no version version numbers should be specified');
79     # fake to stop warnings when calculating $oldx etc
80     @ARGV = qw(99.99.99 99.99.99);
81 }
82 elsif ($opts{c}) {
83     @ARGV == 1 or usage('required one version number');
84     push @ARGV, $ARGV[0];
85 }
86 else {
87     @ARGV == 2 or usage('require two version numbers');
88 }
89 usage('only one of -c, -s, -u and -i') if keys %opts > 1;
90
91 my ($oldx, $oldy, $oldz) = $ARGV[0] =~ /^(\d+)\.(\d+)\.(\d+)$/
92         or usage("bad version: $ARGV[0]");
93 my ($newx, $newy, $newz) = $ARGV[1] =~ /^(\d+)\.(\d+)\.(\d+)$/
94         or usage("bad version: $ARGV[1]");
95
96 my $old_decimal = sprintf "%d.%03d%03d", $oldx, $oldy, $oldz; # 5.011001
97
98 # each entry is
99 #   0 a regexp that matches strings that might contain versions;
100 #   1 a sub that returns two strings based on $1 etc values:
101 #     * string containing captured values (for -c)
102 #     * a string containing the replacement value
103 #   2 what we expect the sub to return as its first arg; undef implies
104 #     don't match
105 #   3 a regex restricting which files this applies to (undef is all files)
106 #
107 # Note that @maps entries are checks in order, and only the first to match
108 # is used.
109
110 my @maps =  (
111     [
112         qr{^((?:api_)?version(?:=|\s+)'?) (\d+) ('?) (?!\.)}x,
113         sub { $2, "$1$newy$3" },
114         $oldy,
115         qr/config/,
116     ],
117     [
118         qr{^(subversion(?:=|\s+)'?) (\d+) ('?)  (?!\.)}x,
119         sub { $2, "$1$newz$3" },
120         $oldz,
121         qr/config/,
122     ],
123     [
124         qr{^(api_subversion(?:=|\s+)'?) (\d+) ('?)  (?!\.)}x,
125         sub { $2, ($newy % 2) ? "$1$newz$3" : "${1}0$3" },
126         ($oldy % 2) ? $oldz : 0,
127         qr/config/,
128     ],
129     [
130         qr{^(api_versionstring(?:=|\s+)'?) ([\d\.]+) ('?) (?!\.)}x,
131         sub { $2, ($newy % 2) ? "$1$newx.$newy.$newz$3": "$1$newx.$newy.0$3" },
132         ($oldy % 2) ? "$oldx.$oldy.$oldz" : "$oldx.$oldy.0",
133         qr/config/,
134     ],
135     [
136         qr{(version\s+'?) (\d+) ('?\s+subversion\s+'?) (\d+) ('?)  (?!\.)}x,
137         sub { "$2-$4", "$1$newy$3$newz$5" },
138         "$oldy-$oldz",
139         qr/config/,
140     ],
141     [
142         qr{\b (PERL_(?:API_)?VERSION(?:=|\s+)'?) (\d+) ('?)  (?!\.)}x,
143         sub { $2, "$1$newy$3"},
144         $oldy,
145     ],
146     [
147         qr{\b (PERL_SUBVERSION(?:=|\s+)'?) (\d+) ('?)  (?!\.)}x,
148         sub { $2, "$1$newz$3"},
149         ($oldy % 2) ? $oldz : 0,
150     ],
151     [
152         qr{\b (PERL_API_SUBVERSION(?:=|\s+)'?) (\d+) ('?)  (?!\.)}x,
153         sub { $2, ($newy % 2) ? "$1$newz$3" : "${1}0$3" },
154         $oldz,
155     ],
156     # these two formats are in README.vms
157     [
158         qr{\b perl-(\d+\^\.\d+\^\.\d+) \b}x,
159         sub { $1, "perl-$newx^.$newy^.$newz"},
160         undef,
161     ],
162     [
163         qr{\b ($oldx _ $oldy _$oldz) \b}x,
164         sub { $1, ($newx . '_' . $newy . '_' . $newz)},
165         undef,
166     ],
167     # 5.8.9
168     [
169         qr{ $oldx\.$oldy\.$oldz \b}x,
170         sub {"", "$newx.$newy.$newz"},
171         undef,
172     ],
173
174     # 5.008009
175     [
176         qr{ $old_decimal \b}x,
177         sub {"", sprintf "%d.%03d%03d", $newx, $newy, $newz },
178         undef,
179     ],
180
181     # perl511, perl511.dll, perl511.lib, perl511s.lib, libperl511.a
182     [
183         qr{\b ((?:lib)?) perl (\d\d\d) (s?) \b }x,
184         sub {$2, "$1perl$newx$newy$3" },
185         "$oldx$oldy",
186         qr/makedef|win32|hints/,      # makedef.pl, README.win32, win32/*, hints/*
187     ],
188
189     # microperl locations should be bumped for major versions
190     [
191         qr{(/)(\d\.\d{2})(["'/])},
192         sub { $2, "$1$newx.$newy$3" },
193         "$oldx.$oldy",
194         qr/uconfig/,
195     ],
196
197     # rename perl-5^.15^.1.dirperl-5_15_1.dir in README.vms
198     [
199         qr{\sperl-(\d+)_(\d+)_(\d+)\.dir}x,
200         sub { warn "got $& - perl-${1}_${2}_${3}.dir -> perl-${newx}_${newy}_${newz}.dir";  " perl-${1}_${2}_${3}.dir", " perl-${newx}_${newy}_${newz}.dir" },
201         " perl-${oldx}_${oldy}_{$oldz}.dir",
202         qr/README.vms/,
203     ],
204
205 );
206
207
208 # files and dirs that we likely don't want to change version numbers on.
209
210 my %SKIP_FILES = map { ($_ => 1) } qw(
211     Changes
212     MANIFEST
213     Porting/epigraphs.pod
214     Porting/how_to_write_a_perldelta.pod
215     Porting/release_managers_guide.pod
216     Porting/release_schedule.pod
217     Porting/bump-perl-version
218     pod.lst
219     pp_ctl.c
220 );
221 my @SKIP_DIRS = qw(
222     ext
223     lib
224     pod
225     cpan
226     t
227 );
228
229 my @mani_files = sort keys %{ExtUtils::Manifest::maniread('MANIFEST')};
230 my %mani_files = map { ($_ => 1) } @mani_files;
231 die "No entries found in MANIFEST; aborting\n" unless @mani_files;
232
233 if ($opts{c} or $opts{s} or $opts{i}) {
234     do_scan();
235 }
236 elsif ($opts{u}) {
237     do_update();
238 }
239 else {
240     usage('one of -c, -s or -u must be specified');
241 }
242 exit 0;
243
244
245
246
247 sub do_scan {
248     for my $file (@mani_files) {
249         next if grep $file =~ m{$_/}, @SKIP_DIRS;
250         if ($SKIP_FILES{$file}) {
251             warn "(skipping $file)\n";
252             next;
253         }
254         open my $fh, '<', $file;
255         my $header = 0;
256         my @stat = stat $file;
257         my $mode = $stat[2];
258         my $file_changed = 0;
259         my $new_contents = '';
260
261         while (my $line = <$fh>) {
262             my $oldline = $line;
263             for my $map (@maps) {
264                 my ($pat, $sub, $expected, $file_pat) = @$map;
265
266                 next if defined $file_pat and $file !~ $file_pat;
267                 next unless $line =~ $pat;
268                 my ($got, $replacement) = $sub->();
269
270                 if ($opts{c}) {
271                     # only report unexpected 
272                     next unless defined $expected and $got ne $expected;
273                 }
274                 $line =~ s/$pat/$replacement/
275                     or die "Internal error: substitution failed: [$pat]\n";
276             }
277             $new_contents .= $line if $opts{i};
278             if ($line ne $oldline) {
279                 $file_changed = 1;
280                 if ($opts{s}) {
281                     print "\n$file\n" unless $header;
282                     $header=1;
283                     printf "\n%5d: -%s       +%s", $., $oldline, $line;
284                 }
285             }
286         }
287         if ($opts{i} && $file_changed) {
288             warn "Updating $file inplace\n";
289             open my $fh, '>', $file;
290             binmode $fh;
291             print $fh $new_contents;
292             close $fh;
293             chmod $mode & 0777, $file;
294         }
295     }
296     warn "(skipped  $_/*)\n" for @SKIP_DIRS;
297 }
298
299 sub do_update {
300
301     my %changes;
302     my $file;
303     my $line;
304
305     # read in config
306
307     while (<STDIN>) {
308         next unless /\S/;
309         if (/^(\S+)$/) {
310             $file = $1;
311             die "No such file in MANIFEST: '$file'\n" unless $mani_files{$file};
312             die "file already seen; '$file'\n" if exists $changes{$file};
313             undef $line;
314         }
315         elsif (/^\s+(\d+): -(.*)/) {
316             my $old;
317             ($line, $old) = ($1,$2);
318             die "$.: old line without preceding filename\n"
319                             unless defined $file;
320             die "Dup line number: $line\n" if exists $changes{$file}{$line};
321             $changes{$file}{$line}[0] = $old;
322         }
323         elsif (/^\s+\+(.*)/) {
324             my $new = $1;
325             die "$.: replacement line seen without old line\n" unless $line;
326             $changes{$file}{$line}[1] = $new;
327             undef $line;
328         }
329         else {
330             die "Unexpected line at ;line $.: $_\n";
331         }
332     }
333
334     # suck in file contents to memory, then update that in-memory copy
335
336     my %contents;
337     for my $file (sort keys %changes) {
338         open my $fh, '<', $file;
339         binmode $fh;
340         $contents{$file} = [ <$fh> ];
341         chomp @{$contents{$file}};
342         close $fh;
343
344         my $entries = $changes{$file};
345         for my $line (keys %$entries) {
346             die "$file: no such line: $line\n"
347                     unless defined $contents{$file}[$line-1];
348             if ($contents{$file}[$line-1] ne $entries->{$line}[0]) {
349                 die "$file: line mismatch at line $line:\n"
350                         . "File:   [$contents{$file}[$line-1]]\n"
351                         . "Config: [$entries->{$line}[0]]\n"
352             }
353             $contents{$file}[$line-1] = $entries->{$line}[1];
354         }
355     }
356
357     # check the temp files don't already exist
358
359     for my $file (sort keys %contents) {
360         my $nfile = "$file-new";
361         die "$nfile already exists in MANIFEST; aborting\n"
362             if $mani_files{$nfile};
363     }
364
365     # write out the new files
366
367     for my $file (sort keys %contents) {
368         my $nfile = "$file-new";
369         open my $fh, '>', $nfile;
370         binmode $fh;
371         print $fh $_, "\n" for @{$contents{$file}};
372         close $fh;
373
374         my @stat = stat $file;
375         my $mode = $stat[2];
376         die "stat $file fgailed to give a mode!\n" unless defined $mode;
377         chmod $mode & 0777, $nfile;
378     }
379
380     # and rename them
381
382     for my $file (sort keys %contents) {
383         my $nfile = "$file-new";
384         warn "updating $file ...\n";
385         rename $nfile, $file;
386     }
387 }
388