#!/usr/bin/perl -w # A program that installs source packages, and records which files were # installed with the package. # Author: Kevin D. Clark (kdc@alumni.unh.edu) # Copyright 1998 Kevin D. Clark (kdc@alumni.unh.edu) # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version # 2 of the License, or (at your option) any later version. # Usage: type "my-make-install -h" for usage information. # TODO: # Need to add support for fstype in Perl for all interesting platforms. # Clean up the bogus "df" messages. # Might want to add option for saving `make install` output. # Need to make this more useful in the case of installing Perl modules use strict; use File::Find; use Cwd; use Getopt::Std; use vars qw/ $opt_d $opt_n $opt_o $opt_h /; $opt_d = "/"; # the default directory to start the search $opt_n = 0; # 1 means search NFS filesystems, 0 means skip them $opt_o = "see code below"; # filename of logfile $opt_h = 0; # does the user just want help? &main; # since I wrote all of this code, I might as well run it too, huh? ########################################################################### sub main { my $makeTarget = ""; # the default target to make my ($package, $cwd, $defaultOutput); ($package = $cwd = Cwd::getcwd()) =~ s#^.*/([^/]+)$#$1#; $opt_o = $defaultOutput = $ENV{'HOME'} . "/.installed-packages" . "/" . $package . ".log"; getopts("hnd:o:") || (&usage && exit(1)); &usage, exit(0) if ($opt_h); push(@ARGV, "install") if (scalar(@ARGV) == 0); map { $makeTarget .= qq/ "$_" /; } @ARGV; my $MAKE = defined($ENV{'MAKE'}) ? $ENV{'MAKE'} : "make"; if ($opt_o eq $defaultOutput) { # default to putting output in # $ENV{'HOME'}/.installed-packages/{package-name} # As a convenience, we'll automatically create this directory for the user # The ramification of using $HOME here is this: this script is usually is # run as root; this means that the output file *might be* placed in # ~root (this depends on your OS -- even in a SU'd environment, # different things happen to $HOME) if (! -d "$ENV{'HOME'}/.installed-packages") { mkdir("$ENV{'HOME'}/.installed-packages", 0700) || die "Couldn't create $ENV{'HOME'}/.installed-packages: $!\n"; } } open(LOGFILE, ">$opt_o") || die "Couldn't open $opt_o for writing: $!\n"; open(MAKEOUT, "$MAKE $makeTarget 2>&1 |") || die "Couldn't run '$MAKE $makeTarget': $!\n"; print "BEGIN output of '$MAKE $makeTarget'\n"; while () { print; # might be useful somehow? # print LOGFILE; # or some other file } print "END output of '$MAKE $makeTarget'\n"; close (MAKEOUT) || die "Couldn't close 'make $makeTarget': $!"; # Now we find all of the new files on the system. This is complicated by # the fact that other files on the system can get updated by other jobs. # We try to prune a little bit here, but hand-editing this file afterwards # wouldn't be a bad idea. print "Searching under $opt_d for files associated with $package...\n"; File::Find::find(sub{ return if (!-d && !-f _); if (-d _) { ($File::Find::prune = 1, return) if ( $File::Find::name eq "/dev" || $File::Find::name eq "/net" || $File::Find::name eq "/var/spool" || $File::Find::name eq "/usr/spool" || $File::Find::name eq "/proc" || $File::Find::name eq "/var/log" || $File::Find::name eq "/var/cron" || $File::Find::name eq "/var/adm" || $File::Find::name eq "/var/tmp" || $File::Find::name eq "/var/adm" || $File::Find::name eq "/var/mail" || $File::Find::name eq "/mnt/cdrom" || $File::Find::name eq "/cdrom" # Next two have to do with ClearCase || $File::Find::name eq "/vob" || $File::Find::name eq "/view" # More helpful than harmful || $File::Find::name eq "/tmp" # More helpful than harmful || ( $File::Find::name eq $ENV{'HOME'} && $ENV{'HOME'} ne "/") || $_ eq "Mail" || $_ eq "Maildir" || $_ eq "News" || $_ eq ".netscape" || $File::Find::name eq $cwd || $File::Find::name eq $opt_o || (!$opt_n && is_a_remote_fs($File::Find::name))); return; } # Why do this? Perhaps to prevent someone who is over-tired from # looking at the stuff in $opt_o and concluding to themselves that "hey, # I can 'rm -f' all of that stuff when it comes time to uninstall. # (don't forget, if we 'mkdir /usr/local/foo', /usr/local is modified...) return if (! -f _); # next few are emacs files return if (/^#.*#$/); return if (/^.saves-\d+-/); return if ( $_ eq "INBOX" || $_ eq "RMAIL" || $_ eq "RMAIL~" ); # NFS files return if ( $File::Find::name eq "/etc/mnttab" || $File::Find::name eq "/etc/.mnttab.lock" || $File::Find::name eq "/etc/utmp" || $File::Find::name eq "/etc/utmpx" ); print LOGFILE $File::Find::name, "\n" if ((-M _) < 0); }, $opt_d); close (LOGFILE) || die "Couldn't close $opt_o: $!"; print "It is suggested that you hand-edit $opt_o now...\n"; exit(0); } ########################################################################### sub usage { print <<"EOF" NAME my-make-install - used to run "make install" *and* to keep track of which files are created/updated because of this action. USAGE my-make-install [-h] [-n] [-d directory] [targets...] DESCRIPTION A wrapper around "make install", which is a pretty standard way to install a given package from source code. In addition to calling "make install", this program attempts to log exactly which files were created/modified during this process. This information is stored by default in \$HOME/.installed-packages/{package-name} . OPTIONS -d directory Specifies a directory underneath which this program should begin its search. By default this program starts searching from "/", but sometimes when you know what you're doing it's nice to be able to specify something like "-d /usr/local". -h Prints this help and then exits. -n Forces this program to search any NFS mounted directories in its search for files associated with this package. By default this program does *not* search such directories. -o logfile Specifies the output file in which to store the names of the files associated with this package installation. By default, the {package-name} of the package defaults to the name of the directory in which this program is run and the output is placed in \$HOME/.installed-packages/{package-name} . In the default case the directory \$HOME/.installed-packages is created for you. It is sometimes useful to specify this option in cases where the directory name doesn't identify the package very well. targets... A list of make targets to be used instead of the default "install" target. TYPICAL USAGE Instead of simply typing "make install", type "my-make-install". Ninety-nine percent of the time this is all you need to type. On rare occasions you might want to invoke "make" with target(s) other than "install". If this is the case, type something like this: my-make-install binaries info some-other-target Even rarer still are usage of the other options, but these are provided for your convenience. CAVEATS By default this program stores logs in the \$HOME/.installed-packages directory. But \$HOME in this case might not be the \$HOME that you were expecting. Typically, you'll invoke this program just after you've su'd to root. Under some systems (notably Linux), your \$HOME environment variable will change after this happens. This usually isn't a big deal, and if it is, please use the -o option. AUTHOR Kevin D. Clark EOF } ########################################################################### # Pass this function a directory name. # # returns true if the directory is a remote filesystem; false otherwise # # This implementation currently works on Solaris and Linux. Other systems # need to be handled as well. # sub is_a_remote_fs { my($dir) = @_; my($dev,$ino,$mode,$nlink,$uid,$gid); # the usual but imperfect way to determine if this is a remote directory (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($dir)) || (warn "Unable to lstat $dir: $!\n", return 0); return 1 if ($dev < 0); # unfortunately, the above two lines don't work on all platforms # and Perl doesn't yet support the fstype field of the stat structure # so we do this: if ($^O eq "solaris") { my ($fstype) = `df -g "$dir"` =~ /\b(\S+)\s+fstype\b/s; return 1 if ( lc($fstype) eq "nfs" || lc($fstype) eq "autofs"); } elsif ($^O eq "linux") { my ($fstype) = `df -T "$dir"` =~ /Mounted on\s+\S+\s+(\S+)/s; return 1 if ( lc($fstype) eq "nfs" || lc($fstype) eq "autofs"); } else { warn "$0: unable to identify remote directories on this system type\n"; } return 0; }