#!/usr/bin/perl -w
#
# Copyright (c) 2004 Oliver Eikemeier. 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.
#
# 3. Neither the name of the author nor the names of its contributors may be
#    used to endorse or promote products derived from this software without
#    specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED "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
# COPYRIGHT OWNER 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.
#
# $FreeBSD$
#
# MAINTAINER=   eik@FreeBSD.org
#
# depends.pl creates a dependecy database for the ports in the ports tree.
# The files format is file|cvs info|pkgorigin ...| other information ...
#
# Subsequent runs could be fed with a list of modified files, only incrementally
# updating the database and generating a file with a list of affected ports.
#
# This list could be used for incremental package or INDEX builds, or to audit
# the list of changed ports.
#

require 5.005;
use strict;
use File::Find;
use Cwd 'abs_path';

my $portsdir    = $ENV{PORTSDIR}    ? $ENV{PORTSDIR}    : '/usr/ports';
my $dependsdir  = $ENV{DEPENDSDIR}  ? $ENV{DEPENDSDIR}  : '/var/db/chkversion';

my $fileslist   = $ENV{FILESLIST}   ? $ENV{FILESLIST}   : '';
my $portslist   = $ENV{PORTSLIST}   ? $ENV{PORTSLIST}   : '';

my $make        = '/usr/bin/make';

-d $portsdir or die "Can't find ports tree at $portsdir.\n";
$portsdir = abs_path($portsdir);

my $dependsfile = "$dependsdir/DEPENDS";

sub readfrom {
    my $dir = shift;

    if (!open CHILD, '-|') {
        open STDERR, '>/dev/null';
        chdir $dir if $dir;
        exec @_;
        die;
    }
    my @childout = <CHILD>;
    close CHILD;

    map chomp, @childout;

    return wantarray ? @childout : $childout[0];
}

foreach (qw(ARCH OPSYS OSREL OSVERSION PKGINSTALLVER PORTOBJFORMAT UID)) {
    my @cachedenv = readfrom $portsdir, $make, "-V$_";
    $ENV{$_} = $cachedenv[0];
}
$ENV{WITH_OPENSSL_BASE} = 'yes';
$ENV{__MAKE_CONF} = '/dev/null';
$ENV{WRKDIRPREFIX} = '/nonexistent';
$ENV{PORT_DBDIR} = '/nonexistent';

my %includes;
my %cvsinfo;

sub parsemakefile {
    my ($makefile) = @_;

    return
        if exists $includes{$makefile};
    my $i = \%{$includes{$makefile}};
    $cvsinfo{$makefile} = '';

    -f "$portsdir/$makefile" || return;

    open MAKEFILE, "<$portsdir/$makefile"
        or die "Can't read $makefile.\n";
    while (<MAKEFILE>) {
        while (/\\$/) {
            chomp; chop;
            $_ .= <MAKEFILE>;
        }
	if (/^\.\s*include\s*(?:<([^>]*)>|"([^"]*)")/) {
            my $file = $1 ? $1 : $2;
            $i->{$file} = 1
                if $file !~ /^bsd\.port(?:\.pre|\.post)?\.mk$/ &&
                    $file !~ m'^\$\{(?:\.CURDIR|MASTERDIR)\}/(?:\.\./Makefile\.inc|Makefile\.local)$';
        }
        if (m'\$FreeBSD\: [^\$ ]+,v (\d+(?:\.\d+)+) (\d{4}(?:[/-]\d{2}){2} \d{2}(?::\d{2}){2}) (\w+) [\w ]+\$') {
            $cvsinfo{$makefile} = "$1 $2 $3";
        }
    }
    close MAKEFILE;
}

my %dependencies;

sub parseport {
    my ($port) = @_;
    -f "$portsdir/$port/Makefile" || return;

    my @makefiles = ("$port/Makefile");
    my %makevars;

    while (@makefiles) {
        my $makefile = shift @makefiles;

        next
            if $dependencies{$makefile}{$port};
        $dependencies{$makefile}{$port} = 1;

        my ($basedir) = "$portsdir/$makefile" =~ m'^(.*)/[^/]+$';

        parsemakefile $makefile;
        my %i = %{$includes{$makefile}};

        foreach my $dep (keys %i) {
            # this can not cope with nested variables
            my @vars = $dep =~ /\$\{([^{}]+?)\}/g;
            foreach (@vars) {
                $makevars{$_} = undef
                    if !exists $makevars{$_};
            }
        }

        $makevars{'.CURDIR'} = "$portsdir/$port"
            if exists $makevars{'.CURDIR'};
        $makevars{'PORTSDIR'} = $portsdir
            if exists $makevars{'PORTSDIR'};

        foreach my $var (keys %makevars) {
            $makevars{$var} = readfrom "$portsdir/$port",
              $make, "-V$var"
                if !defined $makevars{$var};
        }

        foreach my $dep (keys %i) {
            foreach my $var (keys %makevars) {
                $dep =~ s/\$\{$var\}/$makevars{$var}/g
                    if defined $makevars{$var};
            }
            if ($dep =~ /\$\{([^}]+)\}/) {
                print "Warning ($port): can not expand $1 in $dep.\n";
                next;
            }
            $dep =~ s"^[^/]"$basedir/$&";
            while ($dep =~ s'[^/]+/\.\.(?:/|$)'' || $dep =~ s'//'/') {}
            next if $dep =~ m'^/nonexistent/';
            if ($dep =~ s"^$portsdir/"") {
                next if $dep =~ m'^Mk/';
                push @makefiles, $dep;
            }
            else {
                print "Warning ($port): $dep not in $portsdir.\n";
            }
        }
    }
}

my %depfiles;

if (-f $dependsfile) {
    open DEPENDS, "<$dependsfile"
        or die "Can't read $dependsfile.\n";
    while (<DEPENDS>) {
        my ($makefile, $info, $ports) = split '\|';
        foreach my $port (split ' ', $ports) {
            $dependencies{$makefile}{$port} = 1;
            $depfiles{$port}{$makefile} = 1;
        }
        $cvsinfo{$makefile} = $info;
    }
    close DEPENDS;
}

my %categories;

map { $categories{$_} = 0 }
    split ' ', readfrom $portsdir, $make, '-VSUBDIR';

map { $categories{$_} = 1 } keys %categories
    if !%depfiles;

my %newport;

if ($fileslist && -f $fileslist) {
    open FILES, "<$fileslist"
        or die "Can't read $fileslist.\n";
    while (<FILES>) {
        chomp;
        if (m'^([^/]+)/Makefile(?:\.inc)?$') {
             $categories{$1} = 1
                 if defined $categories{$1};
        }
        elsif ($dependencies{$_}) {
            foreach my $port (keys %{$dependencies{$_}}) {
                $newport{$port} = 1;
            }
        }
    }
    close FILES;
}

foreach my $category (grep $categories{$_}, keys %categories) {
    -f "$portsdir/$category/Makefile" || next;

    my @ports = split ' ',
      readfrom "$portsdir/$category", $make, '-VSUBDIR';

    my %knownports;
    foreach (grep index($_, "$category/") == 0, keys %depfiles) {
        $knownports{$_} = 1;
    }

    foreach (map "$category/$_", @ports) {
        if (defined $knownports{$_}) {
            $knownports{$_} = 0;
        }
        else {
            $newport{$_} = 1;
        }
    }

    foreach (grep $knownports{$_}, keys %knownports) {
        $newport{$_} = 0;
    }
}

foreach my $port (keys %newport) {
    foreach my $makefile (keys %{$depfiles{$port}}) {
        delete $dependencies{$makefile}{$port};
    }
}

foreach my $port (keys %newport) {
    parseport $port
        if $newport{$port} && -f "$portsdir/$port/Makefile";
}

undef %depfiles;

system 'mv', '-f', $dependsfile, "$dependsfile.bak" 
    if -f $dependsfile;

open DEPENDS, ">$dependsfile"
    or die "Can't write $dependsfile.\n";
foreach my $file (sort keys %dependencies) {
    print DEPENDS "$file|$cvsinfo{$file}|", join (' ', sort keys %{$dependencies{$file}}), "|\n"
        if %{$dependencies{$file}};
}
close DEPENDS;

if ($portslist) {
    open PORTSLIST, ">$portslist"
        or die "Can't write $portslist.\n";
    foreach my $port (sort keys %newport) {
        print PORTSLIST $newport{$port} ? '+' : '-', $port, "\n";
    }
    close PORTSLIST;
}

exit 0;
