#!/usr/bin/perl -w ############################################################################## # ----- BEGIN LICENSE BLOCK ----- # Version: MPL 1.1/GPL 2.0/LGPL 2.1 # # The contents of this file are subject to the Mozilla Public License Version # 1.1 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. # # The Original Code is Patch Maker. # # The Initial Developer of the Original Code is # Gervase Markham . # Portions created by the Initial Developer are Copyright (C) 2001 # the Initial Developer. All Rights Reserved. # # Contributor(s): # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), # in which case the provisions of the GPL or the LGPL are applicable instead # of those above. If you wish to allow use of your version of this file only # under the terms of either the GPL or the LGPL, and not to allow others to # use your version of this file under the terms of the MPL, indicate your # decision by deleting the provisions above and replace them with the notice # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ----- END LICENSE BLOCK ----- ############################################################################## ############################################################################## # This is version 0.72 of Patch Maker. $pm_version = "0.72"; # Patch Maker currently requires the following external programs: # unzip # # It also requires a file called chromelist.txt, which is a list of all the # editable files in the chrome, in local form, and their equivalent CVS paths. ############################################################################## ############################################################################## # Terminology: # CVS files = files in a cvs tree # Build files = their equivalents in a built version of the app # 'Files' file = the file with a ".file" extension in your data directory # which contains the list of Build files # patch reference (or patchref) = the tag used to identify the current patch. ############################################################################## use File::Spec; use File::Copy; use File::Basename; use Cwd; ############################################################################## # # Configuration - fill in your values here. # # $editor - the name of your editor # $editor_modal - set to "&" if you have a graphical editor, "" if you have # a text mode editor. ############################################################################## my $editor = "nedit"; # http://www.nedit.org . It rocks. my $editor_modal = "&"; # Remove this for command-line editors. # datadir - where Patch Maker keeps its files. This must exist. If you use a # relative path, it's relative to the working directory. If you follow the # instructions on the web page, you shouldn't need to change this. my $datadir = File::Spec->catdir(File::Spec->updir(), File::Spec->updir(), "pm"); ############################################################################## # # Subroutines # ############################################################################## ############################################################################## # Return the Build files as a list or space-separated string # Returns undef if there are no files. ############################################################################## sub get_files() { open(FILELIST, "<$files_file") or die "$execname: Unable to open '$files_file' for reading. $!\n"; my @files; while (my $key = ) { last if $key eq ""; chomp $key; push @files, ($key); } close FILELIST; return wantarray ? @files : join(" ", @files); } ############################################################################## # Find the CVS file for a given build file. # Returns undef if file is not found. ############################################################################## sub findcvsfile( $ ) { my ($file) = @_; chomp $file; open(CHROMELIST, ") { last if m/$file /; } $_ && s/.*\((.*)\).*\n?/$1/; return $_; } ############################################################################## # Find the build file for a given CVS file. # Returns undef if file is not found. ############################################################################## sub findbuildfile( $ ) { my ($file) = @_; chomp $file; open(CHROMELIST, ") { last if m/$file\)/; } $_ && s/([^\s]+)\s+\(.*\).*\n?/$1/; return $_; } ############################################################################## # Print and run a given command line. ############################################################################## sub pexec( $ ) { my ($exec) = @_; print $exec . "\n"; exec $exec; } ############################################################################## # # Functions implementing Patch Maker commands # ############################################################################## ############################################################################## # Add ############################################################################## sub pmadd( $ ) { my @files = get_files(); my @addfiles; foreach my $buildfile (shift @_ || @ARGV) { # de-metacharacter $buildfile (my $findfile = $buildfile) =~ s/([\[\$\\({}).+?\/])/\\$1/g; if (grep /^$findfile$/, @files) { print "'$buildfile' is already part of this patch.\n"; } else { # find correct match to make sure it's safe to add - # prevent disappointment later my $cvsfile = findcvsfile($buildfile); if ($cvsfile) { print "Adding file '$buildfile'.\n"; copy("$buildfile", "$buildfile.bak"); push @addfiles, $buildfile; } else { print "I was not able to find the CVS equivalent of '$buildfile'.\n"; print "This file was not added to the current patch.\n"; } } } # Add the new files to the end of the list open(FILELIST, ">>$files_file") or die "$execname: Can't open file '$files_file' for appending. $!\n"; foreach my $addfile (@addfiles) { print FILELIST $addfile . "\n"; } close FILELIST; } ############################################################################## # Diff ############################################################################## sub pmdiff() { # All other args to pmdiff get passed to diff my $args = join ' ', @ARGV; my @buildfiles = get_files(); # Create the new diff my $diff = ""; foreach my $file (@buildfiles) { $diff .= `diff -u $args $file.bak $file`; } # If the new diff has zero size, don't clobber the old one. if ($diff ne "") { open(CHROMEDIFF, ">$chromediff_file") or die "$execname: Can't open file '$chromediff_file' for writing. $!\n"; print CHROMEDIFF $diff; close CHROMEDIFF; # Change to the CVS file paths foreach my $buildfile (@buildfiles) { my $cvsfile = findcvsfile($buildfile); if ($cvsfile) { # De-metacharacter $buildfile $buildfile =~ s/([\[\$\\({}).+?\/])/\\$1/g; $diff =~ s/$buildfile/$cvsfile/g; } else { print "Warning: no CVS equivalent found of $buildfile.\n"; print "Your CVS patch may not work correctly.\n"; } } open(DIFF, ">$diff_file") or die "$execname: Can't open file '$diff_file' for writing. $!\n"; print DIFF $diff; close DIFF; print "Patch size is "; print -s "$diff_file"; print " bytes\n"; } else { print "\n*** Warning: diff has zero size. Not overwriting. ***\n\n"; } } ############################################################################## # Edit ############################################################################## sub pmedit() { my @buildfiles = get_files(); my $editfiles = ""; # Arguments to pmedit are interpreted as globs of files in the list - # e.g. pmedit *.cpp . my $regexp = shift @ARGV || ""; if ($regexp) { # Can have more than one glob; you get the file if it matches any. while ($regexp) { # The following code attempts to turn a glob syntax into a regexp one # by first escaping all special regexp symbols from file's pathname $regexp =~ s/([\[\$\\({}).+\/])/\\$1/g; # and converting glob syntax to regexp. $regexp =~ s/\*/.\*/g; $regexp =~ s/\?/.?/g; $regexp =~ s/\#/[0-9]/g; foreach $file (@buildfiles) { if($file =~ /$regexp/i) { $editfiles = $editfiles . '"' . $file . '" '; } } $regexp = shift @ARGV || ""; } } else { # No glob - just open the lot $editfiles = '"' . join('" "', @buildfiles) . '"'; } if ($editfiles eq "") { print "$execname: No filenames match the string given.\n"; exit; } else { $editfiles =~ s/\s+$//; # Fix up the path separator (not needed on Windows) if ($macos) { $editfiles =~ tr/\//:/; } pexec("$editor $editfiles $editor_modal"); } } ############################################################################## # Flist ############################################################################## sub pmflist() { # cat to console (in pairs if -a specified) my @files = get_files(); if (@files) { print "File list for patch '$patchref':\n\n"; foreach my $buildfile (@files) { print $buildfile . "\n"; if (defined($ARGV[0]) && $ARGV[0] eq "-a") { my $cvsfile = findcvsfile($buildfile); $cvsfile && print "-> $cvsfile\n\n"; } } } else { print "Patch '$patchref' has no files yet.\n"; } print "\n"; } ############################################################################## # Grep ############################################################################## sub pmgrep() { my @buildfiles = get_files(); my $pattern = join(" ", @ARGV); print "\n"; foreach my $buildfile (@buildfiles) { open(BUILDFILE, "<$buildfile"); while () { if (m/$pattern/i) { print "* " . $buildfile . " : " . $. . " :\n" . $_ . "\n"; } } close BUILDFILE; } } ############################################################################## # Help ############################################################################## sub pmhelp() { print <<"EOF"; Patch Maker Command Reference ----------------------------- Use either the short form (pmf) or the long form (pmflist) of any command. pmf(list) - prints the list of files in the current patch. Use -a ("all") to get the CVS equivalents also. pmd(iff) - does a diff -u for all the files in the file list. Extra arguments to this command are passed through to diff - for example, "-w". It won't clobber your old diff if the new one turns out to have zero size. It produces both the chrome and CVS versions of the diff, and places them in the data directory. pmv(iew) - brings up the CVS version of your diff in an editor window. This is so you can sanity-check it. pmp(atch) - patches your diff back in to the build you are currently sitting in. Takes a -R to back the patch out again. pme(dit) - brings up in your editor any files in the current patch which match the pattern. Pattern is a simple glob. No arguments opens all files. pmw(hoami) - prints the current patch reference pms(witch) - changes Patch Maker to work with a new patch reference pma(dd) - adds those filenames to the current patch pmr(emove) - removes those filenames from the current patch pmn(ojar) - unjars your chrome. If it's jarred, any other command also does this instead of what you ask for pmg(rep) - searches all added files for a given string, case-insensitively For further documentation, see http://www.mozilla.org/hacking/patch-maker/ . EOF exit; } ############################################################################## # Init ############################################################################## sub pminit() { my @commands = qw{add diff edit flist grep nojar patch remove switch view whoami}; my $force_platform = shift @ARGV || ""; if ($unix || $darwin || $force_platform eq "unix") { # Go to the pm directory $0 =~ /(.*)\//; chdir("$1") or die "$execname: Can't change directory to '$1'. $!\n"; print "Initialising Patch Maker in this directory.\n"; print "Creating symlinks...\n"; foreach $command (@commands) { system("ln -s pm pm$command"); $command =~ m/(.).*/; system("ln -s pm pm$1"); } } elsif ($win32 || $force_platform eq "win") { print "Initialising Patch Maker in this directory.\n"; print "Creating batch files...\n"; my $cwd = getcwd(); foreach $command (@commands) { open(BATFILE, ">pm$command.bat"); print BATFILE "\@perl \"$cwd\\pm\" --$command %1 %2 %3 %4 %5 %6 %7 %8 %9"; close BATFILE; $command =~ m/(.).*/; copy("pm$command.bat", "pm$1.bat"); } } else { print "Your platform does not require initialisation of Patch Manager.\n"; print "You should invoke Patch Manager as e.g. 'pm --add' or 'pm -a'.\n"; } } ############################################################################## # Nojar ############################################################################## sub pmnojar() { while (<*.jar>) { # This is a best guess situation for which chrome we want my $notme = ($macos || $darwin) ? "win|unix" : ($win32 ? "mac|unix" : "mac|win"); next if (m/.*($notme)\.jar/); print $_ . "\n"; # XXX portability system("unzip -q $_"); } # Fix up installed-chrome.txt local $^I = ".orig"; local @ARGV = ("installed-chrome.txt"); while (<>) { s/chrome\/.*jar!/chrome/g; s/jar://g; print; } # Delete the rdf files; they'll get regenerated from installed-chrome.txt. unlink(glob("*.rdf")); } ############################################################################## # Patch # Parameters - arguments to patch ############################################################################## sub pmpatch( $ ) { # All extra args to pmpatch get passed to patch. The most common one to use # in this context is -R. my $args = join(' ', @ARGV) . ' ' . join(' ', @_); $args =~ s/\s+$//; if (!(-e "$chromediff_file")) { if (-e "$diff_file") { # The diff has been imported from elsewhere. Convert to a chromediff. print "This diff has been imported from elsewhere.\n"; print "Converting it for use with Patch Maker...\n\n"; open(DIFF, "<$diff_file") or die "$execname: Can't open file '$diff_file' for writing. $!\n"; open(CHROMEDIFF, ">$chromediff_file") or die "$execname: Can't open file '$chromediff_file' for writing. $!\n"; my ($cvsfile, $buildfile); while (my $line = ) { # We need to try and make it most likely that we find the right build # file for the CVS file, even if the patch doesn't include complete # path information. If it's a CVS diff, it'll have an RCS file: line # with the full info on it. if ($line =~ m/^RCS file: \/cvsroot\/mozilla\/(.+),.*/) { $cvsfile = $1; $buildfile = findbuildfile($cvsfile); } if ($line =~ m/^(\+\+\+)\s+([^\s]+)\s+.*/) { # If it's not a CVS diff, all we'll have is the +++ and --- lines. # This is the case with a patch from another Patch Maker. In that # case, we just have to take what we have, and try to match it. # This is not 100%, but it should be good enough. $cvsfile = $2; if (!$buildfile) { $buildfile = findbuildfile($cvsfile); } if ($buildfile) { # 'touch' open(FILELIST, ">>$files_file") or die "$execname: Can't touch file '$files_file'. $!\n"; close FILELIST; pmadd($buildfile); # De-metacharacter $cvsfile # XXX - do we need to do the same for $buildfile? $cvsfile =~ s/([\[\$\\({}).+?\/])/\\$1/g; $line =~ s/$cvsfile/$buildfile/; } else { print "Warning: Unable to find a local match for '$cvsfile'.\n"; print "This patch is not suitable for use with Patch Maker.\n"; exit; } } print CHROMEDIFF $line; } close CHROMEDIFF; close DIFF; } } if (-e "$chromediff_file") { pexec("patch -p0 $args < $chromediff_file"); } else { print "Couldn't find a patch for patch reference '$patchref' to apply.\n"; } } ############################################################################## # Remove ############################################################################## sub pmremove() { my @buildfiles = get_files(); # We need to find which removefiles are in the buildfiles list. If any are # not, we need to print an error. # Convert lists to hashes with value 1 my %buildfiles; my %removefiles; foreach my $buildfile (@buildfiles) { $buildfiles{$buildfile} = 1; } foreach my $removefile (@ARGV) { $removefiles{$removefile} = 1; } # Set both hash values to 0 on all matches foreach my $buildfile(keys %buildfiles) { if (defined($removefiles{$buildfile})) { $buildfiles{$buildfile} = 0; $removefiles{$buildfile} = 0; print "Removing file '$buildfile'.\n"; copy("$buildfile.bak", "$buildfile"); } } # Print an error for removefiles which are still 1 foreach my $removefile (keys %removefiles) { if ($removefiles{$removefile} == 1) { print "File '$removefile' is not part of this patch.\n"; } } # Rewrite the file with the buildfiles which are still 1 open(FILELIST, ">$files_file") or die "$execname: Can't open file '$files_file' for writing. $!\n"; foreach my $buildfile (keys %buildfiles) { if ($buildfiles{$buildfile} == 1) { print FILELIST $buildfile . "\n"; } } } ############################################################################## # Switch ############################################################################## sub pmswitch() { # Do we have an argument? $new_patchref = shift @ARGV; if ($new_patchref) { open(CURRENT, ">$patchref_file") or die "$execname: Can't open file '$patchref_file' for writing. $!\n"; print CURRENT $new_patchref; print "You were working on patch '$patchref'.\n"; print "You are now working on patch '$new_patchref'.\n"; close CURRENT; $files_file = File::Spec->catfile($datadir, "$new_patchref.files"); # 'touch' open(FILELIST, ">>$files_file") or die "$execname: Can't touch file '$files_file'. $!\n"; close FILELIST; } else { print "$execname: No new patch reference given.\n"; exit; } } ############################################################################## # View ############################################################################## sub pmview() { if (defined($ARGV[0]) && $ARGV[0] eq "-c") { pexec("$editor \"$chromediff_file\" $editor_modal"); } else { pexec("$editor \"$diff_file\" $editor_modal"); } } ############################################################################## # Whoami ############################################################################## sub pmwhoami() { print "You are currently working on patch '$patchref'.\n"; } ############################################################################## # # Main code. # ############################################################################## # Work out my platform. # BSD bit is a wild-ass guess. $unix = ($^O eq "linux" || $^O =~ /bsd/i) ? 1 : 0; $darwin = ($^O eq "darwin") ? 1 : 0; $macos = ($^O eq "MacOS") ? 1 : 0; $win32 = ($^O eq "MSWin32" || $^O eq "cygwin") ? 1 : 0; # Work out what command I am executing by looking at my executed name $execname = basename($0); $command = basename($0); if ($command eq "pm" || $command eq "pm.pl") { # We have been invoked as "pm -" or "pm --" rather than # via a symlink. So we need to calculate the command to execute from the # first parameter. $command = shift @ARGV; if (defined($command)) { # We need to make the -d or --diff match the command names, so # cut off initial dashes if present. $command =~ s/^--?//; } } else { # We have been invoked via a symlink. Cut off "pm" $command =~ s/^pm//; } if (!$command) { print "$execname: No command name given.\n"; exit; } # These have to be done before the following tests, as they don't have to be # executed in a chrome directory. if ($command eq "init") { pminit(); exit; } if ($command eq "version") { print "\nPatch Maker version $pm_version.\n"; print "Author: Gervase Markham .\n"; print "See: http://www.mozilla.org/hacking/patch-maker/\n\n"; exit; } # Check we are in the right place. if (!(-e "installed-chrome.txt")) { print "There is no installed-chrome.txt in this directory. Are you sure "; print "you are in a Mozilla chrome directory?\n"; exit; } if (!(-e "chromelist.txt")) { print "Unable to find chromelist.txt. Please make sure it shipped "; print "with this build.\n"; exit; } $patchref_file = File::Spec->catfile($datadir, "patchref"); # check for existence of datadir. if (!(-d $datadir)) { mkdir $datadir, 0777; } # create patchref-file at first run if (!(-e $patchref_file)) { # "echo" open(CURRENT, ">$patchref_file") or die "$execname: Can't open file '$patchref_file' for writing. $!\n"; print CURRENT "__no_patch_reference__"; close CURRENT; } # If chrome not unjarred, unjar it. if (!(-d "content")) { if ($command ne "nojar" || $command ne "n") { print "Your chrome is not unjarred. That needs to be done first.\n\n"; } print "Unjarring chrome...\n\n"; pmnojar(); print "\nChrome unjarred.\n"; if ($command ne "nojar" || $command ne "n") { print "Please issue that command again.\n\n"; } exit; } # Get the current patch reference (always needed) open(CURRENT, "<$patchref_file") or die "$execname: Can't open file '$patchref_file' for reading. $!\n"; $patchref = ; close CURRENT; chomp $patchref; $files_file = File::Spec->catfile($datadir, "$patchref.files"); $chromediff_file = File::Spec->catfile($datadir, "$patchref.chromediff"); $diff_file = File::Spec->catfile($datadir, "$patchref.diff"); # Execute the correct command. if ($command eq "add" || $command eq "a") { pmadd(undef); } elsif ($command eq "diff" || $command eq "d") { pmdiff(); } elsif ($command eq "edit" || $command eq "e") { pmedit(); } elsif ($command eq "flist" || $command eq "f") { pmflist(); } elsif ($command eq "grep" || $command eq "g") { pmgrep(); } elsif ($command eq "help" || $command eq "h") { pmhelp(); } elsif ($command eq "nojar" || $command eq "n") { pmnojar(); } elsif ($command eq "patch" || $command eq "p") { pmpatch(""); } elsif ($command eq "remove" || $command eq "r") { pmremove(); } elsif ($command eq "switch" || $command eq "s") { pmswitch(); } elsif ($command eq "view" || $command eq "v") { pmview(); } elsif ($command eq "whoami" || $command eq "w") { pmwhoami(); } else { print "$execname: Command '$command' not recognised.\n"; } exit;