#!/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) # # The -i option can be used to combine these two steps (if you prefer to make # all of the changes at once and then edit the results via git). # 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 of false positives (eg pod/*). These are listed on # STDERR as they are skipped. use strict; use warnings; use autodie; use Getopt::Std; use ExtUtils::Manifest; sub usage { die < -s -u -i -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 -i scan files and make changes inplace 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('csui', \%opts) or usage; if ($opts{u}) { @ARGV == 0 or usage('no version version numbers should be specified'); # 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, -u and -i') 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, ($newy % 2) ? "$1$newz$3" : "${1}0$3" }, ($oldy % 2) ? $oldz : 0, qr/config/, ], [ qr{^(api_versionstring(?:=|\s+)'?) ([\d\.]+) ('?) (?!\.)}x, sub { $2, ($newy % 2) ? "$1$newx.$newy.$newz$3": "$1$newx.$newy.0$3" }, ($oldy % 2) ? "$oldx.$oldy.$oldz" : "$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"}, ($oldy % 2) ? $oldz : 0, ], [ qr{\b (PERL_API_SUBVERSION(?:=|\s+)'?) (\d+) ('?) (?!\.)}x, sub { $2, ($newy % 2) ? "$1$newz$3" : "${1}0$3" }, $oldz, ], # 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{ $oldx\.$oldy\.$oldz \b}x, sub {"", "$newx.$newy.$newz"}, undef, ], # 5.008009 [ qr{ $old_decimal \b}x, sub {"", sprintf "%d.%03d%03d", $newx, $newy, $newz }, undef, ], # perl511, perl511.dll, perl511.lib, perl511s.lib, libperl511.a [ qr{\b ((?:lib)?) perl (\d\d\d) (s?) \b }x, sub {$2, "$1perl$newx$newy$3" }, "$oldx$oldy", qr/win32|hints/, # README.win32, win32/*, hints/* ], # microperl locations should be bumped for major versions [ qr{(/)(\d\.\d{2})(["'/])}, sub { $2, "$1$newx.$newy$3" }, "$oldx.$oldy", qr/uconfig/, ], # win32/Makefile.ce [ qr/(PV\s*=\s*)(\d\d{2})\b$/, sub { $2, "$1$newx$newy" }, "$oldx$oldy", qr/Makefile\.ce/, ], ); # files and dirs that we likely don't want to change version numbers on. my %SKIP_FILES = map { ($_ => 1) } qw( Changes MANIFEST Porting/Maintainers.pl Porting/acknowledgements.pl Porting/corelist-perldelta.pl Porting/epigraphs.pod Porting/how_to_write_a_perldelta.pod Porting/release_managers_guide.pod Porting/release_schedule.pod Porting/bump-perl-version pp_ctl.c ); my @SKIP_DIRS = qw( ext lib pod cpan 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} or $opts{i}) { do_scan(); } elsif ($opts{u}) { do_update(); } else { usage('one of -c, -s or -u must be specified'); } 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; my $header = 0; my @stat = stat $file; my $mode = $stat[2]; my $file_changed = 0; my $new_contents = ''; while (my $line = <$fh>) { my $oldline = $line; for my $map (@maps) { my ($pat, $sub, $expected, $file_pat) = @$map; next if defined $file_pat and $file !~ $file_pat; next unless $line =~ $pat; my ($got, $replacement) = $sub->(); if ($opts{c}) { # only report unexpected next unless defined $expected and $got ne $expected; } $line =~ s/$pat/$replacement/ or die "Internal error: substitution failed: [$pat]\n"; } $new_contents .= $line if $opts{i}; if ($line ne $oldline) { $file_changed = 1; if ($opts{s}) { print "\n$file\n" unless $header; $header=1; printf "\n%5d: -%s +%s", $., $oldline, $line; } } } if ($opts{i} && $file_changed) { warn "Updating $file inplace\n"; open my $fh, '>', $file; binmode $fh; print $fh $new_contents; close $fh; chmod $mode & 0777, $file; } } warn "(skipped $_/*)\n" for @SKIP_DIRS; } sub do_update { my %changes; my $file; my $line; # read in config while () { 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 preceding 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; binmode $fh; $contents{$file} = [ <$fh> ]; chomp @{$contents{$file}}; close $fh; 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; binmode $fh; print $fh $_, "\n" for @{$contents{$file}}; close $fh; my @stat = stat $file; my $mode = $stat[2]; die "stat $file fgailed to give a mode!\n" unless defined $mode; chmod $mode & 0777, $nfile; } # and rename them for my $file (sort keys %contents) { my $nfile = "$file-new"; warn "updating $file ...\n"; rename $nfile, $file; } }