The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (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.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
package Apache::TestBuild;

use strict;
use warnings FATAL => 'all';

use subs qw(system chdir
            info warning);

use Config;
use File::Spec::Functions;
use File::Path ();
use Cwd ();

use constant DRYRUN => 0;

my @min_modules = qw(access auth log-config env mime setenvif
                     mime autoindex dir alias);

my %shared_modules = (
    min  => join(' ', @min_modules),
);

my %configs = (
    all => {
        'apache-1.3' => [],
        'httpd-2.0' => enable20(qw(modules=all proxy)),
    },
    most => {
        'apache-1.3' => [],
        'httpd-2.0' => enable20(qw(modules=most)),
    },
    min => {
        'apache-1.3' => [],
        'httpd-2.0' => enable20(@min_modules),
    },
    exp => {
        'apache-1.3' => [],
        'httpd-2.0' => enable20(qw(example case_filter
                                   case_filter_in cache
                                   echo deflate bucketeer)),
    },
);

my %builds = (
     default => {
         cflags => '-Wall',
         config => {
             'apache-1.3' => [],
             'httpd-2.0'  => [],
         },
     },
     debug => {
         cflags => '-g',
         config => {
             'apache-1.3' => [],
             'httpd-2.0'  => [qw(--enable-maintainer-mode)],
         },
     },
     prof => {
         cflags => '-pg -DGPROF',
     },
     shared => {
         config =>  {
             'apache-1.3' => [],
             'httpd-2.0'  => enable20_shared('all'),
         },
     },
     mostshared => {
         config =>  {
             'apache-1.3' => [],
             'httpd-2.0'  => enable20_shared('most'),
         },
     },
     minshared => {
         config =>  {
             'apache-1.3' => [],
             'httpd-2.0'  => enable20_shared('min'),
         },
     },
     static => {
     },
);

my %mpms = (
    default => [qw(prefork worker)],
    MSWin32 => [qw(winnt)],
);

my @cvs = qw(httpd-2.0 apache-1.3);

my @dirs = qw(build tar src install);

sub enable20 {
    [ map { "--enable-$_" } @_ ];
}

sub enable20_shared {
    my $name = shift;
    my $modules = $shared_modules{$name} || $name;
    enable20(qq(mods-shared="$modules"));
}

sub default_mpms {
    $mpms{ $^O } || $mpms{'default'};
}

sub default_dir {
    my($self, $dir) = @_;
    $self->{$dir} ||= catdir $self->{prefix}, $dir,
}

sub new {
    my $class = shift;

    #XXX: not generating a BUILD script yet
    #this way we can run:
    #perl Apache-Test/lib/Apache/TestBuild.pm --cvsroot=anon --foo=...

    require Apache::TestConfig;
    require Apache::TestTrace;
    Apache::TestTrace->import;

    my $self = bless {
        prefix => '/usr/local/apache',
        cwd => Cwd::cwd(),
        cvsroot => 'cvs.apache.org:/home/cvs',
        cvs => \@cvs,
        cvstag => "",
        ssldir => "",
        mpms => default_mpms(),
        make => $Config{make},
        builds => {},
        name => "",
        extra_config => {
            'httpd-2.0' => [],
        },
        @_,
    }, $class;

    #XXX
    if (my $c = $self->{extra_config}->{'2.0'}) {
        $self->{extra_config}->{'httpd-2.0'} = $c;
    }

    for my $dir (@dirs) {
        $self->default_dir($dir);
    }

    if ($self->{ssldir}) {
        push @{ $self->{extra_config}->{'httpd-2.0'} },
          '--enable-ssl', "--with-ssl=$self->{ssldir}";
    }

    $self;
}

sub init {
    my $self = shift;

    for my $dir (@dirs) {
        mkpath($self->{$dir});
    }
}

use subs qw(symlink unlink);
use File::Basename;
use File::Find;

sub symlink_tree {
    my $self = shift;

    my $httpd = 'httpd';
    my $install = "$self->{install}/bin/$httpd";
    my $source  = "$self->{build}/.libs/$httpd";

    unlink $install;
    symlink $source, $install;

    my %dir = (apr => 'apr',
               aprutil => 'apr-util');

    for my $libname (qw(apr aprutil)) {
        my $lib = "lib$libname.so.0.0.0";
        my $install = "$self->{install}/lib/$lib";
        my $source  = "$self->{build}/srclib/$dir{$libname}/.libs/$lib";

        unlink $install;
        symlink $source, $install;
    }

    $install = "$self->{install}/modules";
    $source  = "$self->{build}/modules";

    for (<$install/*.so>) {
        unlink $_;
    }

    finddepth(sub {
        return unless /\.so$/;
        my $file = "$File::Find::dir/$_";
        symlink $file, "$install/$_";
    }, $source);
}

sub unlink {
    my $file = shift;

    if (-e $file) {
        print "unlink $file\n";
    }
    else {
        print "$file does not exist\n";
    }
    CORE::unlink($file);
}

sub symlink {
    my($from, $to) = @_;
    print "symlink $from => $to\n";
    unless (-e $from) {
        print "source $from does not exist\n";
    }
    my $base = dirname $to;
    unless (-e $base) {
        print "target dir $base does not exist\n";
    }
    CORE::symlink($from, $to) or die $!;
}

sub cvs {
    my $self = shift;

    my $cmd = "cvs -d $self->{cvsroot} @_";

    if (DRYRUN) {
        info "$cmd";
    }
    else {
        system $cmd;
    }
}

my %cvs_names = (
    '2.0' => 'httpd-2.0',
    '1.3' => 'apache-1.3',
);

my %cvs_snames = (
    '2.0' => 'httpd',
    '1.3' => 'apache',
);

sub cvs_up {
    my($self, $version) = @_;

    my $name = $cvs_names{$version};

    my $dir = $self->srcdir($version);

    if ($self->{cvsroot} eq 'anon') {
        $self->{cvsroot} = ':pserver:anoncvs@cvs.apache.org:/home/cvspublic';
        unless (-d $dir) {
            #XXX do something better than doesn't require prompt if
            #we already have an entry in ~/.cvspass
            #$self->cvs('login');

            warning "may need to run the following command ",
                    "(password is 'anoncvs')";
            warning "cvs -d $self->{cvsroot} login";
        }
    }

    if (-d $dir) {
        chdir $dir;
        $self->cvs(up => "-dP $self->{cvstag}");
        return;
    }

    my $co = checkout($name);
    $self->$co($name, $dir);

    my $post = post_checkout($name);
    $self->$post($name, $dir);
}

sub checkout_httpd_2_0 {
    my($self, $name, $dir) = @_;

    my $tag = $self->{cvstag};

    $self->cvs(co => "-d $dir $tag $name");
    chdir "$dir/srclib";
    $self->cvs(co => "$tag apr apr-util");
}

sub checkout_apache_1_3 {
    my($self, $name, $dir) = @_;

    $self->cvs(co => "-d $dir $self->{cvstag} $name");
}

sub post_checkout_httpd_2_0 {
    my($self, $name, $dir) = @_;
}

sub post_checkout_apache_1_3 {
}

sub canon {
    my $name = shift;
    return $name unless $name;
    $name =~ s/[.-]/_/g;
    $name;
}

sub checkout {
    my $name = canon(shift);
    \&{"checkout_$name"};
}

sub post_checkout {
    my $name = canon(shift);
    \&{"post_checkout_$name"};
}

sub cvs_update {
    my $self = shift;

    my $cvs = shift || $self->{cvs};

    chdir $self->{src};

    for my $name (@$cvs) {
        $self->cvs_up($name);
    }
}

sub merge_build {
    my($self, $version, $builds, $configs) = @_;

    my $b = {
        cflags => $builds{default}->{cflags},
        config => [ @{ $builds{default}->{config}->{$version} } ],
    };

    for my $name (@$builds) {
        next if $name eq 'default'; #already have this

        if (my $flags = $builds{$name}->{cflags}) {
            $b->{cflags} .= " $flags";
        }
        if (my $cfg = $builds{$name}->{config}) {
            if (my $vcfg = $cfg->{$version}) {
                push @{ $b->{config} }, @$vcfg;
            }
        }
    }

    for my $name (@$configs) {
        my $cfg = $configs{$name}->{$version};
        next unless $cfg;
        push @{ $b->{config} }, @$cfg;
    }

    if (my $ex = $self->{extra_config}->{$version}) {
        push @{ $b->{config} }, @$ex;
    }

    if (my $ex = $self->{extra_cflags}->{$version}) {
        $b->{config} .= " $ex";
    }

    $b;
}

my @srclib_dirs = qw(
    apr apr-util apr-util/xml/expat pcre
);

sub install_name {
    my($self, $builds, $configs, $mpm) = @_;

    return $self->{name} if $self->{name};

    my $name = join '-', $mpm, @$builds, @$configs;

    if (my $tag = $self->cvs_name) {
        $name .= "-$tag";
    }

    $name;
}

#currently the httpd-2.0 build does not properly support static linking
#of ssl libs, force the issue
sub add_ssl_libs {
    my $self = shift;

    my $ssldir = $self->{ssldir};

    return unless $ssldir and -d $ssldir;

    my $name = $self->{current_install_name};

    my $ssl_mod = "$name/modules/ssl";
    info "editing $ssl_mod/modules.mk";

    if (DRYRUN) {
        return;
    }

    my $ssl_mk = "$self->{build}/$ssl_mod/modules.mk";

    open my $fh, $ssl_mk or die "open $ssl_mk: $!";
    my @lines = <$fh>;
    close $fh;

    for (@lines) {
        next unless /SH_LINK/;
        chomp;
        $_ .= " -L$ssldir -lssl -lcrypto\n";
        info 'added ssl libs';
        last;
    }

    open $fh, '>', $ssl_mk or die $!;
    print $fh join "\n", @lines;
    close $fh;
}

sub cvs_name {
    my $self = shift;

    if (my $tag = $self->{cvstag}) {
        $tag =~ s/^-[DAr]//;
        $tag =~ s/\"//g;
        $tag =~ s,[/ :],_,g; #-D"03/29/02 07:00pm"
        return $tag;
    }

    return "";
}

sub srcdir {
    my($self, $src) = @_;

    my $prefix = "";
    if ($src =~ s/^(apache|httpd)-//) {
        $prefix = $1;
    }
    else {
        $prefix = $cvs_snames{$src};
    }

    if ($src =~ /^\d\.\d$/) {
        #release version will be \d\.\d\.\d+
        if (my $tag = $self->cvs_name) {
            $src .= "-$tag";
        }
        $src .= '-cvs';
    }

    join '-', $prefix, $src;
}

sub configure_httpd_2_0 {
    my($self, $src, $builds, $configs, $mpm) = @_;

    $src = $self->srcdir($src);

    chdir $self->{build};

    my $name = $self->install_name($builds, $configs, $mpm);

    $self->{current_install_name} = $name;

    $self->{builds}->{$name} = 1;

    if ($self->{fresh}) {
        rmtree($name);
    }
    else {
        if (-e "$name/.DONE") {
            warning "$name already configured";
            warning "rm $name/.DONE to force";
            return;
        }
    }

    my $build = $self->merge_build('httpd-2.0', $builds, $configs);

    $ENV{CFLAGS} = $build->{cflags};
    info "CFLAGS=$ENV{CFLAGS}";

    my $prefix = "$self->{install}/$name";

    rmtree($prefix) if $self->{fresh};

    my $source = "$self->{src}/$src";

    my @args = ("--prefix=$prefix",
                "--with-mpm=$mpm",
                "--srcdir=$source",
                @{ $build->{config} });

    chdir $source;
    system "./buildconf";

    my $cmd = "$source/configure @args";

    chdir $self->{build};

    mkpath($name);
    chdir $name;

    for my $dir (@srclib_dirs) {
        mkpath("srclib/$dir");
    }

    for my $dir (qw(build docs/conf)) {
        mkpath($dir);
    }

    system $cmd;

    open FH, ">.DONE" or die "open .DONE: $!";
    print FH scalar localtime;
    close FH;

    chdir $self->{prefix};

    $self->add_ssl_libs;
}

sub make {
    my($self, @cmds) = @_;

    push @cmds, 'all' unless @cmds;

    for my $name (keys %{ $self->{builds} }) {
        chdir "$self->{build}/$name";
        for my $cmd (@cmds) {
            system "$self->{make} $cmd";
        }
    }
}

sub system {
    my $cmd = "@_";

    info $cmd;
    return if DRYRUN;

    unless (CORE::system($cmd) == 0) {
        my $status = $? >> 8;
        die "system $cmd failed (exit status=$status)";
    }
}

sub chdir {
    my $dir = shift;
    info "chdir $dir";
    CORE::chdir $dir;
}

sub mkpath {
    my $dir = shift;

    return if -d $dir;
    info "mkpath $dir";

    return if DRYRUN;
    File::Path::mkpath([$dir], 1, 0755);
}

sub rmtree {
    my $dir = shift;

    return unless -d $dir;
    info "rmtree $dir";

    return if DRYRUN;
    File::Path::rmtree([$dir], 1, 1);
}

sub generate_script {
    my($class, $file) = @_;

    $file ||= catfile 't', 'BUILD';

    my $content = join '', <DATA>;

    Apache::Test::basic_config()->write_perlscript($file, $content);
}

unless (caller) {
    $INC{'Apache/TestBuild.pm'} = __FILE__;
    eval join '', <DATA>;
    die $@ if $@;
}

1;
__DATA__
use strict;
use warnings FATAL => 'all';

use lib qw(Apache-Test/lib);
use Apache::TestBuild ();
use Getopt::Long qw(GetOptions);
use Cwd ();

my %options = (
    prefix  => "checkout/build/install prefix",
    ssldir  => "enable ssl with given directory",
    cvstag  => "checkout with given cvs tag",
    cvsroot => "use 'anon' for anonymous cvs",
    version => "apache version (e.g. '2.0')",
    mpms    => "MPMs to build (e.g. 'prefork')",
    flavor  => "build flavor (e.g. 'debug shared')",
    modules => "enable modules (e.g. 'all exp')",
    name    => "change name of the build/install directory",
);

my %opts;

Getopt::Long::Configure(qw(pass_through));
#XXX: could be smarter here, being lazy for the moment
GetOptions(\%opts, map "$_=s", sort keys %options);

if (@ARGV) {
    print "passing extra args to configure: @ARGV\n";
}

my $home = $ENV{HOME};

$opts{prefix}  ||= join '/', Cwd::cwd(), 'farm';
#$opts{ssldir}  ||= '';
#$opts{cvstag}  ||= '';
#$opts{cvsroot} ||= '';
$opts{version} ||= '2.0';
$opts{mpms}    ||= 'prefork';
$opts{flavor}  ||= 'debug-shared';
$opts{modules} ||= 'all-exp';

#my @versions = qw(2.0);

#my @mpms = qw(prefork worker perchild);

#my @flavors  = ([qw(debug shared)], [qw(prof shared)],
#                [qw(debug static)], [qw(prof static)]);

#my @modules = ([qw(all exp)]);

my $split = sub { split '-', delete $opts{ $_[0] } };

my @versions = $opts{version};

my @mpms = $split->('mpms');

my @flavors  = ([ $split->('flavor') ]);

my @modules  = ([ $split->('modules') ]);

my $tb = Apache::TestBuild->new(fresh => 1,
                                %opts,
                                extra_config => {
                                    $opts{version} => \@ARGV,
                                });

$tb->init;

for my $version (@versions) {
    $tb->cvs_update([ $version ]);

    for my $mpm (@mpms) {
        for my $flavor (@flavors) {
            for my $mods (@modules) {
                $tb->configure_httpd_2_0($version, $flavor,
                                         $mods, $mpm);
                $tb->make(qw(all install));
            }
        }
    }
}