#!/usr/bin/perl -w

# Copyright (c) 2004-2011 Matthew Seaman. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#    1.  Redistributions of source code must retain the above
#        copyright notice, this list of conditions and the following
#        disclaimer.
#
#    2.  Redistributions in binary form must reproduce the above
#        copyright notice, this list of conditions and the following
#        disclaimer in the documentation and/or other materials
#        provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS''
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

# @(#) $Id: find-updated 803 2011-03-13 11:49:30Z matthew $
#

# Scan through the ports tree, and pick out all ports containing files
# updated after the date given on the command line.  Optionally print
# names of any ports in the cache that no-longer exist in the tree.
# This program doesn't modify the cache at all, but it reads cache
# data and produces output that cache-update can use.

use strict;
use warnings;
use BerkeleyDB;
use File::Find;
use POSIX qw(strftime);

use FreeBSD::Portindex::Config qw(read_config);
use FreeBSD::Portindex::Port;
use FreeBSD::Portindex::Tree;

our %Config;
our $pkgname = 'portindex';

$0 =~ s@.*/@@;    # Script name for error messages

#
# Given a port origin, check for files with a more recent modification
# time than the reference time. If one is found, add this path to the
# list of paths to pass to cache-update.  Also add any ports that
# include an updated Makefile.  Return once all files have been
# checked.  Don't check ./work or other WRKDIR type directories, and
# ignore README.html as those are generated files.
#
sub look_for_updated_files ($$$$)
{
    my $path       = shift;
    my $refmtime   = shift;
    my $updaters   = shift;
    my $tree       = shift;
    my $is_updated = 0;

    File::Find::find(
        {
            preprocess => sub {
                return grep { !/^(work|README\.html)/ } @_;
            },
            wanted => sub {
                my ($mtime) = ( lstat($_) )[9];
                if ( -f _ && $mtime > $refmtime ) {
                    $is_updated++;
                    map { $updaters->{$_}++ }
                      @{ $tree->makefile_list($File::Find::name) };

                    warn "$0: ",
                      strftime( '%Y.%m.%d.%H.%M.%S', localtime($mtime) ),
                      " $File::Find::name\n"
                      if $::Config{Verbose};
                }
              }
        },
        $path
    );
    return $is_updated;
}

#
# Scan through the port directories by reading the SUBDIR variables
# out of port Makefiles.  Remarkably sumilar to
# FreeBSD::Portindex::Tree::make_describe() -- that function and this
# one should be amalgamated somehow.
#
sub scan_makefiles ($$$$$)
{
    my $path     = shift;
    my $refmtime = shift;
    my $updaters = shift;
    my $allports = shift;
    my $tree     = shift;
    my $pk;
    my $sd;
    my @subdirs;

    # Hmmm... Using make(1) to print out the value of the variable
    # (make -V SUBDIRS) takes about 200 times as long as just scanning
    # the Makefiles for definitions of the SUBDIR variable.  Be picky
    # about the format of the SUBDIR assignment lines: SUBDIR is used
    # in some of the leaf Makefiles, but in a different style.

    chdir $path
      or do {
        warn "$0: Error. Can't change directory to $path -- $!\n";
        return;
      };

    open( MAKE, "/usr/bin/make -V PKGNAME -V SUBDIR|" )
      or do {

        # If $path does not exist, or if there's no Makefile there,
        # then something has gone horribly wrong.  Give up trying to
        # recurse at this directory level.

        warn "$0: Error. Can't run make at $path -- $!";
        return;    # Leave out this directory.
      };
    chomp( $pk = <MAKE> );    # If this is set, it's a port
    chomp( $sd = <MAKE> );
    close MAKE
      or do {

        # Even if the close() errors out, we've got this far, so
        # might as well carry on and try and process any output.

        warn "$0: Error. $path: ",
          ( $! ? "close failed -- $!\n" : "make -- bad exit status -- $?\n" );
      };

    # Note that we've been here.
    $allports->{$path}++;

    if ($pk) {

        # Keep track of which ports we've seen: anything we don't see
        # but that is known in the cache must be a deleted or
        # disconnected port.  Anything we do see, but that isn't known
        # in the cache must be a new port.

        warn "$0: new port found at \'$path\'\n"
          if ( $::Config{Warnings} && !exists $allports->{$path} );

        # This is a real port directory, not a subdir.

        $updaters->{$path}++
          if look_for_updated_files( $path, $refmtime, $updaters, $tree );

    } else {

        @subdirs = map { "${path}/$_" } split( ' ', $sd );

        for my $subdir (@subdirs) {

            # Recursion means never having to obey a function
            # prototype.

            &scan_makefiles( $subdir, $refmtime, $updaters, $allports, $tree );
        }
    }
    return;
}

#
# Add all of the port origins known in the cache, but not visited
# during scan_makefiles() to the list updated stuff (on the basis that
# those must be ports that have been deleted)
#
sub deleted_ports($$$$)
{
    my $updaters = shift;
    my $allports = shift;
    my $tree     = shift;
    my $refmtime = shift;

    for my $origin ( keys %{$allports} ) {
        if ( $allports->{$origin} == 0 ) {

            # A port can be unhooked from the ports system, but the
            # directory left in the tree.  This means the cache will
            # still contain the port data -- don't pass the name to
            # cache-update to recheck unless that directory contains
            # modified files.  On the other hand, the port directory
            # can be removed completely, and cache-update should look
            # at that directory so that it will similarly modify the
            # cache.

            if ( !-d $origin ) {
                $updaters->{$origin}++;
                warn "$0: $origin has been deleted\n"
                  if $::Config{Warnings};
            } elsif (
                look_for_updated_files( $origin, $refmtime, $updaters, $tree ) )
            {
                $updaters->{$origin}++;
                warn
                  "$0: $origin was updated but is not connected to ports tree\n"
                  if $::Config{Warnings};
            }
        }
    }
    return;
}

MAIN:
{
    my $tree;
    my %updaters;
    my $reference_date;
    my %allports;

    read_config( \%Config );

    $tree = FreeBSD::Portindex::Tree->new(
        -Env           => { -Home => $Config{CacheDir}, },
        -CacheFilename => $Config{CacheFilename},
    );

    # Redirect STDOUT if required
    if ( $Config{Output} ne '-' ) {
        open STDOUT, '>', $Config{Output}
          or die "$0: Can't open output \'$Config{Output}\' -- $!\n";
    }

    $tree->port_origins( \%allports );
    $tree->init_makefile_list();

    # Check the standard ports makefiles in /usr/ports/Mk -- updates
    # to these generally affect a lot of ports.

    look_for_updated_files( "$Config{PortsDir}/Mk", $Config{ReferenceTime},
        \%updaters, $tree );

    # Scan through all ports, looking for files newer than the cut-off
    # date.

    scan_makefiles( $Config{PortsDir}, $Config{ReferenceTime}, \%updaters,
        \%allports, $tree );

    # Add all of the deleted ports to the list of stuff to update

    deleted_ports( \%updaters, \%allports, $tree, $Config{ReferenceTime} );

    print join( "\n", sort keys %updaters ), "\n"
      if %updaters;
}

__END__


=head1 NAME

find-updated -- generate a list of those ports depending on files that
have been modified since the given date/time.

=head1 SYNOPSIS

B<find-updated> [B<-hvq>] [B<-c> F<dir>] [B<-C> F<file>] [B<-T> F<file>] [B<-p> F<dir>] [B<-o> F<file>] date

=head1 DESCRIPTION

B<find-updated> scans through the whole ports tree and checks each
port origin and the F</usr/ports/Mk> directory (or F<${PORTSDIR}/Mk>
if C<$PORTSDIR> is set in the environment) for files modified after
the date given on the command line.  The output consists of a list of
modified ports, printed one per line, suitable for feeding into
B<cache-update> using plain format.  The generated list of updated
ports takes into account any master-slave relationships between ports
and any ports that include Makefiles modified in the given period.

Only file modification times are checked, not directories.  Files
named F<README.html> or any F<work> subdirectories are ignored.

The date specification must be given strictly in the B<cvsup>-style of
I<yyyy.mm.dd.HH.MM.SS> with four digits for the year, and two each for
the month, day of the month, hour (24 hour clock), minutes and
seconds.  The local timezone is assumed.  To print out a date in the
required format, use:
	
	date +%Y.%m.%d.%H.%M.%S

B<find-update> does a further level of checking by comparing the list
of port origins obtained from the cache with all of the ports it scans
for modifications.  Any ports listed in the cache, but either not
present in the ports tree (a I<deleted> port), or not referenced from
the sub-directory F<Makefile>s and containing modified files (a
I<disconnected> port) will be added to the output.  B<cache-update>
will recognise if the port has been deleted or not and modify the
cache accordingly.

=head2 Configuration Files

B<find-updated> shares configuration files with B<portindex>,
B<cache-init> and B<cache-update>.  Any configuration settings are
taken from the following locations, where the later items on this list
override the earlier:

=over 8

=item *

Built-in settings from the B<FreeBSD::Portindex::Config> perl module.

=item *

The system wide configuration file F</usr/local/etc/portindex.cfg>

=item *

The per-user configuration file F<${HOME}/.portindexrc>.  This file is
ignored if the process is run as root.

=item *

The local configuration file, found in the current working directory
of the B<cache-init> process F<./.portindexrc>.  This file is ignored
if the process is run as root.

=item *

The program command line.

=back

All of the configuration files are optional.  A summary of the
resultant configuration options including the effect of any command
line settings is printed as part of the help text when B<portindex> is
invoked with the C<-h> option.

=head1 OPTIONS

=over 8

=item B<-h>

=item B<--help>

Print a brief usage message and a summary of the configuration
settings after command line processing and then exit.

=item B<-v>

=item B<--verbose>

Turn on verbose output printed to C<STDERR>.  This is the default.

=item B<-q>

=item B<--quiet>

=item B<--noverbose>

Turn off verbose output to C<STDERR>.  Using both the B<-v> amd B<-q>
options together does not make any sense, but neither does it generate
an error.  The last mentioned of the two options will prevail.

=item B<-c> F<dir>

=item B<--cache-dir>=F<dir>

The location of the B<portindex> data cache, by default
F</var/db/portindex>.

=item B<-C> F<file>

=item B<--cache-file>=F<file>

Berkeley DB Btree file containing the cached and post-processed values
of a number of C<make> variables for all of the ports in the tree.
This file name will be relative to the cache directory (B<-c> option
above) unless an absolute path is given.  Defaults to
F<portindex-cache.db>.

=item B<-T> F<file>

=item B<--timestamp-file>=F<file>

A file within the cache directory whose modification time marks the
last time that data was modified in or added to the cache.  Defaults
to F<portindex-timestamp>

=item B<-p> F<dir>

=item B<--ports-dir>=F<dir>

The location of the ports tree. Almost always defaults to
F</usr/ports> unless C<$PORTSDIR> is set in the environment.

=item B<-o> F<file>

=item B<--output>=F<file>

Filename to write the generated list of updated ports to.  Setting this to
F<-> means output to STDOUT, which is the default.

=back

=head1 FILES

=over 16

=item F</usr/ports>

The default ports directory.

=item F</var/db/portindex>

The location of the data cache.

=item F<portindex-cache.db>

Btree file containing cached C<make describe> output.

=item F<__db.001>, F<__db.002>, F<__db.003>, F<__db.004>

Files used as part of the internal workings of BerkeleyDB, for memory
pool management and DB locking.  Will be recreated automatically if
deleted.

=item F<portindex-timestamp>

This file contains the last time and date that the cache was updated
or modified.

=item F</usr/local/etc/portindex.cfg>

System-wide configuration file.

=item F<${HOME}/.portindexrc>

Per-user configuration file

=item F<./.portindexrc>

Local configuration file

=back

=head1 SEE ALSO

L<portindex(1)>, L<cache-init(1)>, L<cache-update(1)>, L<cvsup(1)>,
L<ports(7)>

=head1 BUGS

B<find-updated> does not distinguish ports that are I<not connected> --
ie. that do not have an entry in the C<SUBDIR> list in the immediately
superior category.  Such ports will have an entry in the cache, but the
default is not to include them in the resulting F<INDEX>.

=cut

#
# That's All Folks!
#
