add Porting/bump-perl-version
David Mitchell [Tue, 14 Jul 2009 22:22:37 +0000 (23:22 +0100)]
utility for bumping the value of the perl version in lots of files

Porting/bump-perl-version [new file with mode: 0755]

diff --git a/Porting/bump-perl-version b/Porting/bump-perl-version
new file mode 100755 (executable)
index 0000000..4d4df83
--- /dev/null
@@ -0,0 +1,339 @@
+#!/usr/bin/perl
+#
+# bump-perl-version, DAPM 14 Jul 2009
+#
+# A utility to find, and optionally bump, references to the perl version
+# number in various files within the perl source
+#
+# It's designed to work in two phases. First, when run with -s (scan),
+# it searches all the files in MANIFEST looking for strings that appear to
+# match the current perl version (or which it knows are *supposed* to
+# contain the current version), and produces a list of them to stdout,
+# along with a suggested edit. For example:
+#
+#     $ Porting/bump-perl-version -s 5.10.0 5.10.1 > /tmp/scan
+#     $ cat /tmp/scan
+#     Porting/config.sh
+#     
+#     52: -archlib='/opt/perl/lib/5.10.0/i686-linux-64int'
+#         +archlib='/opt/perl/lib/5.10.1/i686-linux-64int'
+#     ....
+#
+# At this point there will be false positives. Edit the file to remove
+# those changes you don't want made. Then in the second phase, feed that
+# list in, and it will change those lines in the files:
+#
+#     $ Porting/bump-perl-version -u < /tmp/scan
+#
+# (so line 52 of Porting/config.sh is now updated)
+
+# This utility 'knows' about certain files and formats, and so can spot
+# 'hidden' version numbers, like PERL_SUBVERSION=9.
+#
+# A third variant makes use of this knowledge to check that all the things
+# it knows about are at the current version:
+#
+#    $ Porting/bump-perl-version -c 5.10.0
+#
+# XXX this script hasn't been tested against a major version bump yet,
+# eg 5.11.0 to 5.12.0; there may be things it missed - DAPM 14 Jul 09
+#
+# Note there are various files and directories that it skips; these are
+# ones that are unlikely to contain anything needing bumping, but which
+# will generate lots fo false positives (eg pod/*). These are listed on
+# STDERR as they are skipped.
+
+use strict;
+use warnings;
+use Getopt::Std;
+use ExtUtils::Manifest;
+
+
+sub usage { die <<EOF }
+
+@_
+
+usage: $0 -c <C.C.C>
+          -s <C.C.C> <N.N.N>
+         -u
+
+    -c check files and warn if any known string values (eg
+       PERL_SUBVERSION) don't match the specified version
+
+    -s scan files and produce list of possible change lines to stdout
+
+    -u read in the scan file from stdin, and change all the lines specified
+
+    C.C.C the current perl version, eg 5.10.0
+    N.N.N the new     perl version, eg 5.10.1
+EOF
+
+my %opts;
+getopts('csu', \%opts) or usage;
+if ($opts{u}) {
+    @ARGV == 0 or usage('no version version numbers should be speciied');
+    # fake to stop warnings when calculating $oldx etc
+    @ARGV = qw(99.99.99 99.99.99);
+}
+elsif ($opts{c}) {
+    @ARGV == 1 or usage('required one version number');
+    push @ARGV, $ARGV[0];
+}
+else {
+    @ARGV == 2 or usage('require two version numbers');
+}
+usage('only one of -c, -s and -u') if keys %opts > 1;
+
+my ($oldx, $oldy, $oldz) = $ARGV[0] =~ /^(\d+)\.(\d+)\.(\d+)$/
+       or usage("bad version: $ARGV[0]");
+my ($newx, $newy, $newz) = $ARGV[1] =~ /^(\d+)\.(\d+)\.(\d+)$/
+       or usage("bad version: $ARGV[1]");
+
+my $old_decimal = sprintf "%d.%03d%03d", $oldx, $oldy, $oldz; # 5.011001
+
+# each entry is
+#   0 a regexp that matches strings that might contain versions;
+#   1 a sub that returns two strings based on $1 etc values:
+#     * string containing captured values (for -c)
+#     * a string containing the replacement value
+#   2 what we expect the sub to return as its first arg; undef implies
+#     don't match
+#   3 a regex restricting which files this applies to (undef is all files)
+#
+# Note that @maps entries are checks in order, and only the first to match
+# is used.
+
+my @maps =  (
+    [
+       qr{^((?:api_)?version(?:=|\s+)'?) (\d+) ('?) (?!\.)}x,
+       sub { $2, "$1$newy$3" },
+       $oldy,
+       qr/config/,
+    ],
+    [
+       qr{^(subversion(?:=|\s+)'?) (\d+) ('?)  (?!\.)}x,
+       sub { $2, "$1$newz$3" },
+       $oldz,
+       qr/config/,
+    ],
+    [
+       qr{^(api_subversion(?:=|\s+)'?) (\d+) ('?)  (?!\.)}x,
+       sub { $2, "${1}0$3" },
+       0,
+       qr/config/,
+    ],
+    [
+       qr{^(api_versionstring(?:=|\s+)'?) ([\d\.]+) ('?) (?!\.)}x,
+       sub { $2, "$1$newx.$newy.0$3" },
+       "$oldx.$oldy.0",
+       qr/config/,
+    ],
+    [
+       qr{(version\s+'?) (\d+) ('?\s+subversion\s+'?) (\d+) ('?)  (?!\.)}x,
+       sub { "$2-$4", "$1$newy$3$newz$5" },
+       "$oldy-$oldz",
+       qr/config/,
+    ],
+    [
+       qr{\b (PERL_(?:API_)?VERSION(?:=|\s+)'?) (\d+) ('?)  (?!\.)}x,
+       sub { $2, "$1$newy$3"},
+       $oldy,
+    ],
+    [
+       qr{\b (PERL_SUBVERSION(?:=|\s+)'?) (\d+) ('?)  (?!\.)}x,
+       sub { $2, "$1$newz$3"},
+       $oldz,
+    ],
+    [
+       qr{\b (PERL_API_SUBVERSION(?:=|\s+)'?) (\d+) ('?)  (?!\.)}x,
+       sub { $2, "${1}0$3"},
+       0,
+    ],
+    # these two formats are in README.vms
+    [
+       qr{\b perl-(\d+\^\.\d+\^\.\d+) \b}x,
+       sub { $1, "perl-$newx^.$newy^.$newz"},
+       undef,
+    ],
+    [
+       qr{\b ($oldx _ $oldy _$oldz) \b}x,
+       sub { $1, ($newx . '_' . $newy . '_' . $newz)},
+       undef,
+    ],
+    # 5.8.9
+    [
+       qr{\b $oldx\.$oldy\.$oldz \b}x,
+       sub {"", "$newx.$newy.$newz"},
+       undef,
+    ],
+
+    # 5.008009
+    [
+       qr{\b $old_decimal \b}x,
+       sub {"", sprintf "%d.%03d%03d", $newx, $newy, $newz },
+       undef,
+    ],
+
+);
+
+
+# files and dirs that we likely don't want to change version numbers on.
+
+my %SKIP_FILES = map { ($_ => 1) } qw(
+    Changes
+    MANIFEST
+    Porting/how_to_write_a_perldelta.pod
+    Porting/mergelog
+    Porting/mergelog-tool
+    pod.lst
+);
+my @SKIP_DIRS = qw(
+    ext
+    lib
+    pod
+    t
+);
+
+my @mani_files = sort keys %{ExtUtils::Manifest::maniread('MANIFEST')};
+my %mani_files = map { ($_ => 1) } @mani_files;
+die "No entries found in MANIFEST; aborting\n" unless @mani_files;
+
+if ($opts{c} or $opts{s}) {
+    do_scan();
+}
+elsif ($opts{u}) {
+    do_update();
+}
+else {
+    usage('one of -c, -s or -u must be specifcied');
+}
+exit 0;
+
+
+
+
+sub do_scan {
+    for my $file (@mani_files) {
+       next if grep $file =~ m{$_/}, @SKIP_DIRS;
+       if ($SKIP_FILES{$file}) {
+           warn "(skipping $file)\n";
+           next;
+       }
+       open my $fh, '<', $file or die "Aborting: can't open $file: $!\n";
+       my $header = 0;
+
+       while (<$fh>) {
+           for my $map (@maps) {
+               my ($pat, $sub, $expected, $file_pat) = @$map;
+
+               next if defined $file_pat and $file !~ $file_pat;
+               next unless $_ =~ $pat;
+               my ($got, $replacement) = $sub->();
+
+               if ($opts{c}) {
+                   # only report unexpected 
+                   next unless defined $expected and $got ne $expected;
+               }
+               my $newstr = $_;
+               $newstr =~ s/$pat/$replacement/
+                   or die "Internal error: substitution failed: [$pat]\n";
+               if ($_ ne $newstr) {
+                   print "\n$file\n" unless $header;
+                   $header=1;
+                   printf "\n%5d: -%s       +%s", $., $_, $newstr;
+               }
+               last;
+           }
+       }
+    }
+    warn "(skipped  $_/*)\n" for @SKIP_DIRS;
+}
+
+sub do_update {
+
+    my %changes;
+    my $file;
+    my $line;
+
+    # read in config
+
+    while (<STDIN>) {
+       next unless /\S/;
+       if (/^(\S+)$/) {
+           $file = $1;
+           die "No such file in MANIFEST: '$file'\n" unless $mani_files{$file};
+           die "file already seen; '$file'\n" if exists $changes{$file};
+           undef $line;
+       }
+       elsif (/^\s+(\d+): -(.*)/) {
+           my $old;
+           ($line, $old) = ($1,$2);
+           die "$.: old line without preceeding filename\n"
+                           unless defined $file;
+           die "Dup line number: $line\n" if exists $changes{$file}{$line};
+           $changes{$file}{$line}[0] = $old;
+       }
+       elsif (/^\s+\+(.*)/) {
+           my $new = $1;
+           die "$.: replacement line seen without old line\n" unless $line;
+           $changes{$file}{$line}[1] = $new;
+           undef $line;
+       }
+       else {
+           die "Unexpected line at ;line $.: $_\n";
+       }
+    }
+
+    # suck in file contents to memory, then update that in-memory copy
+
+    my %contents;
+    for my $file (sort keys %changes) {
+       open my $fh, '<', $file or die "open '$file': $!\n";
+       $contents{$file} = [ <$fh> ];
+       chomp @{$contents{$file}};
+       close $fh or die "close: '$file': $!\n";
+
+       my $entries = $changes{$file};
+       for my $line (keys %$entries) {
+           die "$file: no such line: $line\n"
+                   unless defined $contents{$file}[$line-1];
+           if ($contents{$file}[$line-1] ne $entries->{$line}[0]) {
+               die "$file: line mismatch at line $line:\n"
+                       . "File:   [$contents{$file}[$line-1]]\n"
+                       . "Config: [$entries->{$line}[0]]\n"
+           }
+           $contents{$file}[$line-1] = $entries->{$line}[1];
+       }
+    }
+
+    # check the temp files don't already exist
+
+    for my $file (sort keys %contents) {
+       my $nfile = "$file-new";
+       die "$nfile already exists in MANIFEST; aborting\n"
+           if $mani_files{$nfile};
+    }
+
+    # write out the new files
+
+    for my $file (sort keys %contents) {
+       my $nfile = "$file-new";
+       open my $fh, '>', $nfile or die "create '$nfile' failed: $!\n";
+       print $fh $_, "\n" for @{$contents{$file}};
+       close $fh or die "failed to close $nfile; aborting: $!\n";
+
+       my @stat = stat $file or die "Can't stat $file: $!\n";
+       my $mode = $stat[2];
+       die "stat $file fgailed to give a mode!\n" unless defined $mode;
+       chmod $mode & 0777, $nfile or die "chmod $nfile failed; aborting: $!\n";
+    }
+
+    # and rename them
+
+    for my $file (sort keys %contents) {
+       my $nfile = "$file-new";
+       warn "updating $file ...\n";
+       rename $nfile, $file or die "rename $nfile $file: $!\n";
+    }
+}
+