Subversion Repositories DevTools

Rev

Rev 255 | Rev 279 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

########################################################################
# Copyright (C) 2007 ERG Limited, All rights reserved
#
# Module name   : jats.sh
# Module type   : Makefile system
# Compiler(s)   : n/a
# Environment(s): jats
#
# Description   : Top level Makefile supervisor
#                 The function of this program is to call the target makefiles
#                 in a sutable manner.
#
# History       : Once upon a time this was done with another makefile and
#                 some really ugle shell, embedded within the makefile
#                 This had a number of limitations, including
#                   - Limited to makefile command line syntax
#                   - Overly complex makefile
#                   - Slow execution
#
# Usage:
#
#......................................................................#

require 5.006_001;
use strict;
use warnings;

use Pod::Usage;                             # For help support
use JatsError;
use JatsSystem;
use JatsEnv;
use JatsMakeInfo;
use ReadBuildConfig qw(:All);
use Getopt::Long;
use FileUtils;
use ArrayHashUtils;
use JatsDPackage;

my $VERSION = "1.0.0";                      # Update this

#
#   Options
#
my $opt_debug   = $ENV{'GBE_DEBUG'};        # Allow global debug
my $opt_verbose = $ENV{'GBE_VERBOSE'};      # Allow global verbose
my $opt_help = 0;
my $opt_silent = 1;
my $opt_default;

#
#   Global variables
#
our %cf_info;
our $GBE_TOOLS;                             # Base of tools
our $GBE_ABT;                               # Optional ABT indication
our $GBE_PLATFORM;                          # Optional GBE_PLATFORM
our $ScmInterface;                          # Current interface directory
our $ScmRoot;                               # Build root

my $Tags;                                   # Reference to hash of tags
my %defs;                                   # Make definitions
my @defs;                                   # Make definitions
my @targets;                                # Make targets
my $makelib;                                # Path to makelib.pl
my $update = 0;                             # Makefiles updated
my $build_error = 0;                        # Build Error encountered
my $tlen = 4;                               # Max length of targets
my %common_cmd;                             # Short circuit some commands
my %gbe_platform;                           # Nice form of GBE_PLATFORM

#
#   Known commands
#   A hash of known commands an information about the commands
#       common      - Command is not executed for Debug and Production
#                     Flags: 1 - Command not executed for debug+prod
#                     Flags: 2 - Command is only executed ONCE per platform
#       local       - The command is handled locally.
#       nomakecheck - Don't check for makefile being out of date
#       IFLAG       - iteration flag,
#                       0   No external dependencies.
#                       1   Source dependency list.
#                       2   Shared library dependency list.
#                       3   Application dependency list.
#   Note:   gmake attempts to build includes prior to invoking other
#   rules when the source is non-existent, which can/do fail as a
#   result of the prereq rules not being executed, the IFLAG inhabits
#   includes during these prereq rules.
#
my %composite_commands = (
    #
    #   Composite commands
    #
    'all'                   => [ 'install', 'package', 'deploy' ] ,
    'install'               => [ 'build' ] ,
    'build'                 => [ 'mkdepends', 'generate', 'install_hdr', 'depend',
                                 'make_lib', 'install_lib', 'make_install_shlib',
                                 'make_prog', 'make_test', 'install_prog', 'install_class' ] ,
    'lint'                  => [ 'mkdepends', 'generate_debug', 'install_hdr_debug',
                                 'lint_lib', 'lint_shlib', 'lint_prog' ] ,
    'ctags'                 => [ 'mkdepends', 'generate_debug', 'ctags_debug' ] ,
    'test'                  => [ 'make_test' ] ,
    'mkdepends'             => [ 'makefiles', 'make_init'] ,

    );

my %commands = (
    #
    #   Basic commands / phases of the build process
    #   This should be a list of all known makefile commands
    #
    'make_usage'            => { 'tag' => 0, 'local' => \&DoHelp },
    'help'                  => { 'tag' => 0, 'local' => \&DoHelp },
    'rebuild'               => { 'tag' => 0, 'local' => \&TestMakeFiles },
    'makefiles'             => { 'tag' => 0, 'local' => \&TestMakeFiles },
    'unmakefiles'           => { 'tag' => 0, 'local' => \&UnMakeFiles },
    'make_init'             => { 'tag' => 1, 'nomakecheck' => 1, 'common' => 3, },
    'generate'              => { 'tag' => 1 },
    'install_hdr'           => { 'tag' => 1 },
    'depend'                => { 'tag' => 1, 'unless' => ['NODEPEND','EXPERT'] },
    'make_lib'              => { 'tag' => 1, 'IFLAG' => 1 },
    'make_mlib'             => { 'tag' => 1, 'IFLAG' => 1 },
    'install_lib'           => { 'tag' => 1, 'IFLAG' => 1 },
    'make_install_shlib'    => { 'tag' => 1, 'IFLAG' => 2 },
    'make_prog'             => { 'tag' => 1, 'IFLAG' => 3 },
    'make_test'             => { 'tag' => 1, 'IFLAG' => 3 },
    'install_prog'          => { 'tag' => 1, 'IFLAG' => 3 },
    'install_class'         => { 'tag' => 1 },
    'package'               => { 'tag' => 1, 'IFLAG' => 3 },
    'deploy'                => { 'tag' => 1, 'IFLAG' => 1 },

    #
    #   Special
    #   make_dir is directly invoked by jmake. It doesn't need
    #   to be a part of a command sequence
    #
    'make_dir'              => { 'nomakecheck' => 1, 'tag' => 1 },

    #
    #   Basic commands, that appear to be unused in composite commands
    #   Not too sure why
    #
    'make_script'           => { 'commands' => [], 'tag' => 1 },

    #
    #   Lint related targets
    #
    'lint_lib',             => { 'tag' => 'make_lib'            , 'IFLAG' => 1 },
    'lint_shlib',           => { 'tag' => 'make_install_shlib'  , 'IFLAG' => 2 },
    'lint_prog'             => { 'tag' => 'make_prog'           , 'IFLAG' => 3 },

    #
    #   Following commands should NOT be a part of a composite command
    #
    'run_tests'             => { 'nomakecheck' => 1, 'tag' => 1, 'IFLAG' => 3},
    'run_unit_tests'        => { 'nomakecheck' => 1, 'tag' => 1, 'IFLAG' => 3},
    
    'clean'                 => { 'nomakecheck' => 1},
    'clobber'               => { 'nomakecheck' => 1},
    'rmlitter'              => { 'nomakecheck' => 1},
    'unbuild'               => { 'nomakecheck' => 1},
    'undepend'              => { 'nomakecheck' => 1 , 'tag' => 'depend'},
    'ungenerate'            => { 'nomakecheck' => 1 , 'tag' => 'generate'},
    'uninstall'             => { 'nomakecheck' => 1},
    'uninstall_class'       => { 'nomakecheck' => 1},
    'uninstall_hdr'         => { 'nomakecheck' => 1 , 'tag' => 'install_hdr'},
    'uninstall_lib'         => { 'nomakecheck' => 1 , 'tag' => 'install_lib'},
    'uninstall_prog'        => { 'nomakecheck' => 1 , 'tag' => 'install_prog'},
    'unmake_lib'            => { 'nomakecheck' => 1 , 'tag' => 'make_mlib'},
    'unmake_mlib'           => { 'nomakecheck' => 1 , 'tag' => 'make_mlib'},
    'unmake_prog'           => { 'nomakecheck' => 1 , 'tag' => 'make_prog'},
    'unmake_script'         => { 'nomakecheck' => 1 , 'tag' => 'make_script'},
    'unmake_test'           => { 'nomakecheck' => 1 , 'tag' => 'make_test'},
    'unobj'                 => { 'nomakecheck' => 1},
    'unpackage'             => { 'nomakecheck' => 1 , 'tag' => 'package'},
    'unmake_dir'            => { 'nomakecheck' => 1 , 'tag' => 'make_dir'},

    );
#    DebugDumpData ("Commands", \%commands);

#-------------------------------------------------------------------------------
# Function        : Mainline Entry Point
#
# Description     :
#
# Inputs          :
#

#
#   Parse the user options
#
my $result = GetOptions (
                "help:+"        => \$opt_help,              # flag, multiple use allowed
                "manual:3"      => \$opt_help,              # flag, multiple use allowed
                "verbose:+"     => \$opt_verbose,           # flag, multiple use allowed
                "d|debug:+"     => \$opt_debug,             # flag, multiple use allowed
                "default=s"     => \$opt_default,           # string
                );

                #
                #   UPDATE THE DOCUMENTATION AT THE END OF THIS FILE !!!
                #

#
#   Process help and manual options
#
pod2usage(-verbose => 0, -message => "Version: $VERSION")  if ($opt_help == 1  || ! $result);
pod2usage(-verbose => 1)  if ( $opt_help == 2 );
pod2usage(-verbose => 2)  if ( $opt_help > 2 );

#
#   Configure the error reporting process now that we have the user options
#
ErrorConfig( 'name'    => 'Make',
             'verbose' => $opt_verbose,
             'debug'   => $opt_debug );

#
#   Configure the 'System' process so that it will terminate on any error
#   and not report the complete command line
#
SystemConfig ( 'ExitOnError' => 2);

#
#   Extract non-option commands
#   Much of this is for backward compatability
#   Support the following arguments
#       XXXXX=yyyy      Makefile Definition
#       -XXXX           Flags passed to make
#       XXXX            Raw target name
#
foreach my $arg ( @ARGV )
{
    Verbose2 ("Arg: $arg");
    if ( $arg =~ m~(\w+)=(.+)~ ) {
        $defs{uc($1)} = $2;

    } elsif ( $arg =~ m/^-/ ) {
        push @defs, $arg;

    } else {
        push @targets, $arg;

    }
}

#
#   Misc. Initialisation
#
InitFileUtils();

EnvImport( 'GBE_TOOLS' );
$makelib = "$GBE_TOOLS/makelib.pl";
Error ("Cannot locate JATS file: $makelib") unless ( -f $makelib );

EnvImportOptional ( 'GBE_ABT' );
EnvImportOptional ( 'GBE_PLATFORM' );

#
#   Validate user options
#
@targets = ($opt_default)
    if ( $opt_default && ! @targets);
Error ("No make targets specified" ) unless ( @targets );

#
#   Build the @defs array
#   This is a set of definitions passed to the makefiles
#   It is created on the fly to allow command line options to modify the definitions
#
foreach  ( sort keys %defs )
{
    next if ( m~^DEBUG$~i );                    # Naughty user
    push @defs, "$_=$defs{$_}";                 # Makefile defs are XXXX=aaaa
}

#
#   Look for OPTIONS=*args* and disable the silent make operation
#   This will allow the make to output messages
#
if (( exists $defs{OPTIONS} && $defs{OPTIONS} =~ m/args/ ) || $opt_verbose || $opt_debug )
{
    $opt_silent = 0;
}

#
#   Process GBE_PLATFORM and create a usable hash
#
if ( $GBE_PLATFORM )
{
    Verbose2 ("GBE_PLATFORM: $GBE_PLATFORM");
    $gbe_platform{$_} = 1 foreach ( split( / /, $GBE_PLATFORM ) );
    $gbe_platform{GENERIC} = 1;
}

#
#   Read in the local Makefile.gbe file
#   This will provide a path to the interface directory
#
ReadMakeInfo();

#
#   Read in the global build variables
#
ReadBuildConfig( "$::ScmRoot/$::ScmInterface" );

#
#   Determine the max size of the target name
#   Makes a nice display
#
foreach ( @BUILDPLATFORMS )
{
    my $len = length ($_);
    $tlen = $len if ( $len > $tlen );
}

#-------------------------------------------------------------------------------
#   Scan the list of targets and perform 'alias' expansion
#   targets are of the form
#       platform_command_type
#       Where:
#           type        may be _prod or _debug or not present
#           command     may be either a composite command or a simple command
#           platform    may be a platform or an alias
#
#
foreach my $arg ( @targets )
{
    my $suffix = '';
    my @commands;
    my @platforms;

    #
    #   Remove the only available suffix ( _debug | _prod )
    #   Also detect commnads of debug or prod
    #
    if ( $arg =~ m~(.+)_(debug)$~ ) {
        $suffix = $2;
        $arg = $1;
    } elsif ( $arg =~ m~(.+)_(prod)$~ ) {
        $suffix = $2;
        $arg = $1;
    } elsif ( $arg eq 'debug' ) {
        $suffix = $arg;
        $arg = '';
    } elsif ( $arg eq 'prod' ) {
        $suffix = $arg;
        $arg = '';
    }

    #
    #   Remove valid make commands from the remaining argument
    #   Have a hash of all known makefile commands
    #
    foreach my $cmd ( keys %composite_commands, keys %commands )
    {
        if ( $arg eq $cmd )
        {
            @commands = ExpandComposite($cmd);
            $arg = '';
        }
        elsif ( $arg =~ m~(.+)_($cmd)$~ )
        {
            $arg = $1;
            @commands = ExpandComposite($2);
            last;
        }
    }

    #
    #   See of we can extract an platform alias from the command
    #   Rip into target + command
    #
    if ( $arg )
    {
        if ( exists $ScmBuildAliases{$arg}  )
        {
            @platforms =  split (/ /, $ScmBuildAliases{$arg} );
        }
        elsif ( exists $ScmBuildPlatforms{$arg} )
        {
            @platforms = ($arg);
        }
        else
        {
            if ( @commands )
            {
                Error ("Unknown target: $arg");
            }
            else
            {
                Message ("Unknown command. Passed to make: $arg");
                @commands = $arg;
            }
        }
    }

    #
    #   Insert defaults
    #
    @platforms = @DEFBUILDPLATFORMS
        unless ( @platforms );

    @commands = ExpandComposite('all')
        unless ( @commands);

    #
    #   Perform GBE_PLATFORM filtering
    #
    @platforms = grep ( exists $gbe_platform{$_} , @platforms )
        if ( %gbe_platform );

    Error ("No platforms to be processed. Check GBE_PLATFORM")
        unless ( @platforms );

    #
    #   Iterate commands over platforms
    #
    foreach my $cmd ( @commands )
    {
        PerformCommand (\@platforms, $cmd, $suffix );
    }
}

#
#   All done
#
exit (0);

#-------------------------------------------------------------------------------
# Function        : PerformCommand
#
# Description     : Perform the make on a platform command
#
# Inputs          : $platform_ref   - platforms to process
#                   $cmd            - simple make command
#                   $suffix         - debug,prod,none
#
# Returns         : 
#
sub PerformCommand
{
    my ($platform_ref, $cmd, $suffix) = @_;
    my $do_prod = 1;
    my $do_debug = 1;

    #
    #   Clean up some ugly commands from lint and ctags
    #
    if ( $cmd =~ m~(.+)_(debug)$~ )
    {
        $cmd = $1;
        $suffix = $2;
    }
    Verbose2 ("Processing Target: $cmd, $suffix");

    #
    #   Limit command to production or debug
    #
    if ( $suffix eq 'debug' ) {
        $do_prod  = 0;
    } elsif ( $suffix eq 'prod' ) {
        $do_debug  = 0;
    }

    #
    #   Some commands can be suppressed by options
    #   These are an array of options
    #
    if ( exists $commands{$cmd}{'unless'}  )
    {
        foreach my $check ( @{$commands{$cmd}{'unless'}} )
        {
            if ( exists $defs{$check} && $defs{$check} )
            {
                Verbose2 ("Skipping: $cmd because $check");
                return;
            }
        }
    }

    #
    #   Interecpt commands that are handled locally
    #
    if ( exists $commands{$cmd}{'local'} )
    {
        $commands{$cmd}{'local'}( $cmd, $platform_ref );
        return;
    }

    #
    #   Process and recurse directory tree
    #
    Verbose ("Processing: $cmd, @{$platform_ref}");

    #
    #   Determine the local tag name and state
    #   The tag is used to short circuit makefile commands in recursive makes
    #   It greatly reduces the work required
    #
    #   Many commands have a tag that is the same as the command, but a few don't
    #
    my $tag = 0;
    if ( exists $commands{$cmd}{tag}  )
    {
        $tag = $commands{$cmd}{tag};
        $tag = $cmd if ( $tag eq '1' );
    }

    if ( $commands{$cmd}{'common'} )
    {
        InvokeSubMake( $::Cwd, $platform_ref ,$cmd, $tag, 1 );
    }
    else
    {
        InvokeSubMake( $::Cwd, $platform_ref ,$cmd, $tag, 1 ) if $do_debug;
        InvokeSubMake( $::Cwd, $platform_ref ,$cmd, $tag, 0 ) if $do_prod;
    }
}

#-------------------------------------------------------------------------------
# Function        : InvokeSubMake
#
# Description     : Build recursion
#                   Determine if there is anything to be done for the current
#                   target within a specified directory
#
# Inputs          : $dir
#                   $pref                   - Ref to an array of platforms
#                   $cmd
#                   $tag
#                   $do_prod
#
# Returns         : 
#
sub InvokeSubMake
{
    my ( $dir, $pref, $cmd, $tag, $do_debug) = @_;

    #
    #   Ensure the current directory is known to the Tags database
    #
    $Tags = ReadMaketags( $dir );
    Error ("Directory not in tag database", $dir ) unless ( $Tags );

    #
    #   Process commands in the current directory
    #   If the command is tagged, then we may be able to skip it
    #   Otherwise we need to do it
    #
    InvokeMake( $dir, $pref, $cmd, $tag, $do_debug );

    #
    #   Recurse into any subdirectories that may be a part of the build
    #
    unless ( exists $defs{NORECURSE} && $defs{NORECURSE}  )
    {
        #
        #   Process subdirectories for the current command
        #   If the command is tagged, then we may be able to skip it
        #   Otherwise we need to do it
        #
        foreach my $subdir (@{$Tags->{$dir}{subdirs}} )
        {

            InvokeSubMake( CleanDirName( "$dir/$subdir"), $pref, $cmd, $tag, $do_debug );
        }
    }
}

#-------------------------------------------------------------------------------
# Function        : InvokeMake
#
# Description     : Actually invoke the make for debug and production as required
#
# Inputs          : $dir
#                   $pref
#                   $cmd
#                   $tag
#                   $do_debug
#
#
# Returns         : 
#
sub InvokeMake
{
    my ( $dir, $pref, $cmd, $tag, $do_debug ) = @_;
    my $text = 'C';

    #
    #   Ensure that the current directory actually has makefile targets
    #   Not all makefile.pl create .mk files
    #       1) Top-level makefile.pl
    #       2) Makefiles that use --NoPlatformBuilds
    #
    if ( $Tags->{$dir}{root} )
    {
        Verbose2 "Root directory has no makefiles: $dir";
        return;
    }

    if ( $Tags->{$dir}{noplatforms} )
    {
        Verbose2 "No make targets in $dir";
        return;
    }

    #
    #   Process each platform
    #
    foreach my $target ( @{$pref} )
    {
        unless ( exists $Tags->{$dir}{platforms}{$target} )
        {
            Verbose2 "No make targets in $dir, for $target";
            next;
        }

        #
        #   Do we need to do any thing for this target / tag
        #
        if ( $tag )
        {
            unless ( exists $Tags->{$dir}{full}{$target}{'%MakeTags'}{$tag} )
            {
                Verbose2 ("Skipping $cmd in $dir for $tag");
                next;
            }
        }

        #
        #   common mode == 2
        #   Only process target ONCE for each platform
        #   Don't care when its used
        #
        if ( exists $commands{$cmd}{'common'} && $commands{$cmd}{'common'} & 2 )
        {
            if ( $common_cmd{$cmd}{$target} )
            {
                Verbose2 "Already performed $cmd for $target";
                next;
            }
            $common_cmd{$cmd}{$target} = 1;
        }

        #
        #   Is the build limited to only debug or production
        #
        unless ( exists $commands{$cmd}{'common'} )
        {
            if ( $do_debug == 0 ) {
                next unless ( $Tags->{$dir}{platforms}{$target}{prod} );
                $text = 'P';

            } elsif  ( $do_debug == 1 ) {
                next unless ( $Tags->{$dir}{platforms}{$target}{debug} );
                $text = 'D';
            }
        }

        #
        #   Final sanity test
        #   Must have the makefile. We should have detected this error before now
        #
        Error ("Makefile not found - $target") unless ( -f "$dir/$target.mk" );

        #
        #   Build up the make command line
        #   Examine command specfic flags
        #
        my @args = @defs;
        push @args, "IFLAG=$commands{$cmd}{IFLAG}" if ( exists $commands{$cmd}{IFLAG} );
        push @args, "NOSCMDEPEND=1";

        my $cdir = CleanDirName ($dir);
        my @make_command;
        push @make_command, "$ENV{GBE_BIN}/xmake";
        push @make_command, "-s" if $opt_silent;
        push @make_command, "--no-print-directory";
        push @make_command, "-C", $cdir unless ( $cdir eq $::Cwd );
        push @make_command, "-f", "$target.mk";
        push @make_command, @args;
        push @make_command, 'make_dir' unless exists $commands{$cmd}{nomakecheck} ;
        push @make_command, $cmd;

        Message ( sprintf ("[$text] %-${tlen}s, $cmd, $cdir", $target ));
        System (@make_command, "DEBUG=$do_debug" );
    }
}

#-------------------------------------------------------------------------------
# Function        : TestMakeFiles
#
# Description     : Test all the makefile dependent files to see if any of the
#                   makefiles need to be rebuilt.
#
#                   Walk the directory tree looking for makefiles to be
#                   rebuilt.
#
# Inputs          : $cmd            - Current command
#                   $pref           - Ref to an array of platforms
#
# Returns         : 
#
sub TestMakeFiles
{
    my ( $cmd, $pref ) = @_;
    Verbose ("Test makefile dependencies");

    #
    #   Read in the Tag file for the current directory
    #   This will provide the current list of subdirectories
    #
    #   Process Test the current makefiles, then test any subdirs
    #
    TestSubMake( $cmd, $Cwd, $pref );

    #
    #   Report build errors
    #   Done after all makefiles have been processed
    #
    if ( $build_error )
    {
        Error ("Error creating makefiles. Errors previously reported");
    }

    #
    #   Post makefile build processing
    #   Only required if a files have been re-built
    #
    if ( $update )
    {
        #
        #   Sanity test the makefile structure
        #   Ensure that makefiles have only one parent.
        #
        TestParents();

        #
        #   Sanity test the installed and packaged files
        #   Generate warnings and errors if a file if packaged from two
        #   locations
        #
        TestPackages();

        #
        #   Generate DPACKAGE file if required
        #
        JatsDPackage::DPackageGenerate( $::ScmRoot, $::ScmInterface  );
    }

    #
    #
    #   Check that the build.pl file is not newer that the tags file
    #   This will not be an error, but it will be a nagging warning
    #
    my @build_warn;
    my $bstamp = -M "$::ScmRoot/$ScmBuildSrc";
    my $tstamp = -M "$::ScmRoot/Makefile.gbe";

    push @build_warn, "Missing: Makefile.gbe" unless ( defined $tstamp );
    push @build_warn, "Modified build file ($ScmBuildSrc)" if ( $tstamp && $bstamp < $tstamp );
    if ( @build_warn )
    {
        Warning ("The build file is newer than the generated files",
                 "The project needs to be built for these changes to be included",
                 @build_warn );
    }
}

#-------------------------------------------------------------------------------
# Function        : TestSubMake
#
# Description     : Test the makefiles in the current directory
#                   Recurse into subdirectories
#
# Inputs          : $cmd            - Current command
#                   $dir            - Directory to test
#                   $pref           - Ref to an array of platforms
#
# Returns         : 
#
sub TestSubMake
{
    my ($cmd, $dir, $pref ) = @_;

    $Tags = ReadMaketags( $dir, 1 );
    unless ( $Tags )
    {
        Verbose2 ("TestSubMake :Directory not in tag database", $dir );
        MakeBuild( $dir);
    }
    else
    {
        TestMake( $cmd, $dir, $pref );
    }

    #
    #   Recurse into any subdirectories that may be a part of the build
    #
    unless ( exists $defs{NORECURSE} && $defs{NORECURSE}  )
    {
        foreach my $subdir (@{$Tags->{$dir}{subdirs}} )
        {
            TestSubMake( $cmd, CleanDirName( "$dir/$subdir"), $pref );
        }
    }
}

#-------------------------------------------------------------------------------
# Function        : TestMake
#
# Description     : Test the makefiles component files
#                   and determine if the makefile(s) need to be rebuilt
#
# Inputs          : $cmd            - Current command
#                   $dir            - Directory to test
#                   $pref           - Ref to an array of platforms
#
# Returns         : 
#
sub TestMake
{
    my ( $cmd, $dir, $pref ) = @_;
    my @must_rebuild;

    if ( $cmd eq 'rebuild' )
    {
        MakeBuild( $dir);
        return;
    }

    #
    #   Check that the local makefile.pl is not newer than
    #   the Tags file. Not all makefile.pl's create .mk files
    #
    my $mstamp = -M "$dir/makefile.pl";
    my $tstamp = -M "$dir/Makefile.gbe";

    unless ( defined $tstamp )
    {
        UniquePush (\@must_rebuild, "Missing: $dir/Makefile.gbe");
    }

    if ( $mstamp && $mstamp < $tstamp  )
    {
        UniquePush (\@must_rebuild, "Updated: $dir/makefile.pl");
    }
    else
    {
        if ( $Tags->{$dir}{root} )
        {
            Verbose2 "TestMake: Root directory has no makefiles: $dir";
            return;
        }

        if ( $Tags->{$dir}{noplatforms} )
        {
            Verbose2 "TestMake: No make targets in $dir";
            return;
        }

        #
        #   Process only the required build targets
        #
        foreach my $tgt ( keys %{$Tags->{$dir}{platforms}}   )
        {
            next if ( %gbe_platform && not exists $gbe_platform{$tgt} );
            #
            #   Locate all the makefile dependent files
            #
            my $data = $Tags->{$dir}{full}{$tgt}{'@ScmDepends'};
            if ( $data )
            {
                Verbose2 ("Processing: $dir : $tgt");
                my $base_stamp = -M "$dir/$tgt.mk";
                unless ( defined $base_stamp )
                {
                    UniquePush (\@must_rebuild, "Missing: $dir/$tgt.mk");
                }
                else
                {
                    foreach my $file ( "$dir/makefile.pl" ,@$data )
                    {
                        $file = "$dir/$file" if ( $file =~ m~^\.~ );
                        my $stamp = -M $file;
                        if ( defined $stamp )
                        {
                            Verbose3 ("Timestamp: $stamp, $file");
                            if ( $stamp < $base_stamp  )
                            {
                                UniquePush (\@must_rebuild, $file);
                            }
                        }
                        else
                        {
                            UniquePush (\@must_rebuild, "Missing: $file");
                        }
                    }
                }
            }
            else
            {
                Warning ("No dependency information for: $tgt in $dir");
            }
        }
    }

    if ( @must_rebuild )
    {
        my @display;
        push (@display, CleanDirName($_) ) foreach ( @must_rebuild );
        Message ("One or more JATS source or internal files have changed, Makefiles will be rebuilt",
                  "Target is: $dir",
                  @display );
        MakeBuild ($dir);
    }
    return;
}

#-------------------------------------------------------------------------------
# Function        : MakeBuild
#
# Description     : Rebuild the makefiles in the specified directory
#                   This does not involve recursion - thats already been
#                   done.
#
#                   Once the makefiles have been re-built the tags database
#                   must be read in again as it may have changed.
#
# Inputs          : 
#
# Returns         : 
#
sub MakeBuild
{
    my ($dir) = @_;

    #
    #   No makefiles to build in the root directory
    #   Can't rebuild the top-level Makefile.gbe file
    #
    return if ( $Tags->{$dir}{root} );

    #
    #   Try to rebuild the makefile
    #
    chdir $dir || Error ("Cannot change directory to $dir");
    my $result = System ('--NoExit', $ENV{GBE_PERL}, 'makefile.pl',
                            $::ScmRoot, $makelib, "--interface=$::ScmInterface" );
    chdir $::Cwd || Error ("Cannot change directory to $::Cwd");

    #
    #   Re-read the tags database
    #
    $Tags = ReadMaketags( $dir, 2 );

    #
    #   Flag that makefiles have been modified
    #
    $update = 1;
    $build_error = 1 if ( $result );
}

#-------------------------------------------------------------------------------
# Function        : UnMakeFiles
#
# Description     : Delete generated makefiles and control files
#
# Inputs          : $cmd            - Current command
#                   $pref           - Ref to an array of platforms
#
# Returns         : 
#
sub UnMakeFiles
{
    #
    #   Recurse down the tree
    #   Recurse, then performs operations in the current directory
    #
    sub UnSubMakeFiles
    {
        my ($dir) = @_;
        $Tags = ReadMaketags( $dir, 1 );
        foreach my $subdir (@{$Tags->{$dir}{subdirs}} )
        {
            UnSubMakeFiles( CleanDirName( "$dir/$subdir") );
        }
        UnMake( $dir );
    }

    #
    #   Delete makefiles in the current directory
    #
    sub UnMake
    {
        my ($dir) = @_;
        Verbose("Unmake in $dir");
        foreach my $tgt ( keys %{$Tags->{$dir}{platforms}}   )
        {
            Verbose2("Unmake. Delete ${tgt}.mk");
            unlink ("$dir/${tgt}.mk");
        }
        unlink ("$dir/Makefile.gbe");
        Verbose2("Unmake. Delete Makefile.gbe");
    }

    #
    #   Depth first recursion through the tree
    #
    UnSubMakeFiles ( $Cwd );
}

#-------------------------------------------------------------------------------
# Function        : TestParents
#
# Description     : Ensure that each makefile node has exactly one parent
#                   Prevent the user from creating recursive loops
#
# Inputs          : 
#
# Returns         : 
#
my %parents;
sub TestParents
{
    Verbose ("Test makefile parents");
    #
    #   Init the hash. Command may be invoked multiple times
    #
    %parents = ();

    #
    #   Recurse down the tree
    #   Recurse, then performs operations in the current directory
    #
    sub RecurseDown
    {
        my ($dir) = @_;
        $Tags = ReadMaketags( $dir, 1 );
        Verbose2("TestParents in $dir");
        foreach my $subdir (@{$Tags->{$dir}{subdirs}} )
        {
            my $subdir = CleanDirName( "$dir/$subdir");
            push @{$parents{$subdir}}, $dir;
            RecurseDown( $subdir );
        }
    }

    #
    #   Depth first recursion through the tree
    #
    RecurseDown ( $Cwd );

    #
    #   Test for only one parent
    #   The root makefile won't have any
    #
    foreach my $dir ( sort keys %{$Tags} )
    {
        my $count = $#{$parents{$dir}};
        if ( $count > 0 )
        {
            if ( defined($GBE_ABT) )
            {
                Warning ("Error supressed by ABT");
                Warning ("makefile.pl with multiple parents",
                         "makefile.pl in: $dir",
                         "Parented by:", @{$parents{$dir}} );
            }
            else
            {
                unlink $Tags->{$dir}{config};
                Error ("makefile.pl with multiple parents",
                       "makefile.pl in: $dir",
                       "Parented by:", @{$parents{$dir}} );
            }
        }
    }
}

#-------------------------------------------------------------------------------
# Function        : TestPackages
#
# Description     : Ensure that files that are packaged and installed are
#                   only done from one makefile. Warn if the same file
#                   is being packaged from multiple makefiles.
#
# Inputs          : 
#
# Returns         : 
#
my %test_packages;
sub TestPackages
{
    Verbose ("Test packaged files");
    #
    #   Init the hash. Command may be invoked multiple times
    #
    %test_packages = ();

    #
    #   Recurse down the tree
    #   Recurse, then performs operations in the current directory
    #
    sub TRecurseDown
    {
        my ($dir) = @_;
        Verbose2("TestPackages in $dir");
        $Tags = ReadMaketags( $dir, 1 );
        #
        #   Process makefile
        #   Examine all the %PACKAGE_xxx and %INSTALL_xxx fields thare listed
        #   in the @PACKAGE_VARS and @INSTALL_VARS array
        #
        foreach my $target ( keys %{$Tags->{$dir}{platforms}}   )
        {
            next if ( %gbe_platform && not exists $gbe_platform{$target} );
            foreach my $field ( @{$Tags->{$dir}{full}{$target}{'@PACKAGE_VARS'}},
                                @{$Tags->{$dir}{full}{$target}{'@INSTALL_VARS'}} )
            {
                foreach my $entry ( keys %{$Tags->{$dir}{full}{$target}{$field}} )
                {
                    Verbose3("TestPackages: $target, File: $entry");
                    push @{$test_packages{$target}{$entry}}, $dir;
                }
            }
        }

        #
        #   Process subdirectories too
        #
        foreach my $subdir (@{$Tags->{$dir}{subdirs}} )
        {
            TRecurseDown( CleanDirName( "$dir/$subdir") );
        }
    }



    #
    #   Depth first recursion through the tree
    #
    TRecurseDown ( $Cwd );

    #
    #   Test and report files that are packaged in different makefiles
    #   There are two issues:
    #       1) Not good practice
    #       2) May be different files
    #   The warning message is a bit ugly - but it shouldn't pop up often.
    #
#    DebugDumpData ("test_packages", \%test_packages);

    foreach my $target ( sort keys %test_packages )
    {
        foreach my $file ( sort keys %{$test_packages{$target}} )
        {
            #
            #   The descpkg file is known to be packaged from multiple dirs
            #
            next if ( $file =~ m~/descpkg~ );
            if ( $#{$test_packages{$target}{$file}} > 0 )
            {
                Warning ("File is packaged or installed from multiple makefiles",
                         "Target: $target, Packaged file: $file",
                         "Packaged from the following directories:", @{$test_packages{$target}{$file}} );
            }
        }
    }
}


#-------------------------------------------------------------------------------
# Function        : DoHelp
#
# Description     : Display basic help information
#
# Inputs          : None
#
# Returns         :
#
sub DoHelp
{

#
#   Display basic usage information
#
    pod2usage(-verbose => 0,
              -message => "Version: $VERSION",
              -exitval => 'NOEXIT');

    print "Platform targets\n";
    foreach ( sort keys %BUILDINFO )
    {
        print "    $_\n";
    }

    my @alias = sort keys %ScmBuildAliases;
    if ( $#alias >=0 )
    {
        print "Alias targets\n";
        foreach ( @alias )
        {
            print "    $_   - $ScmBuildAliases{$_}\n";
        }
    }
}

#-------------------------------------------------------------------------------
# Function        : ReadMaketags
#
# Description     : Read in the global make tags data base
#                   This contains information on all the components within
#                   the build. The tags allow recursive makes to be pruned.
#
#                   The file may change while the build is running
#                   It can be read multiple times
#
# Inputs          : $dir            - Directory entry to read
#                   $test           - 1: Read if available
#                                     2: Force read
#
# Returns         : Reference to the tag data for the requested directory
#                   Will error if no available
#
our %cf_minfo;
our %cf_minfo2;
my %tag_data;
sub ReadMaketags
{
    my ($dir, $test) = @_;

    #
    #   Force data re-aquisition
    #
    delete $tag_data{$dir}
        if ( $test && $test == 2 );

    #
    #   Do we already have the entry
    #
    return \%tag_data
        if ( exists ($tag_data{$dir} ));

    #
    #   If the entry is not known, then re-read the index file
    #
    unless ( $::cf_filelist{$dir} )
    {
        my $index_file = "$::ScmRoot/$::ScmInterface/Makefile.cfg";
        Error ("Makefile index missing. Rebuild required") unless ( -f $index_file );

        #
        #   Kill the entry in %INC to force the file to be read in
        #
        $::cf_filelist = ();
        delete $INC{ $index_file };
        require $index_file;
    }

    my $cfg_filen = $::cf_filelist{$dir};
    unless ( $cfg_filen )
    {
        return undef
            if ( $test );
        Error ("Makefile index entry missing: $dir. Rebuild required");
    }


    #
    #   Read in all the Makefile_x.cfg files
    #
    %cf_minfo = ();
    %cf_minfo2 = ();
    my $cfg_file = "$::ScmRoot/$::ScmInterface/$cfg_filen";

    unless ( -f $cfg_file )
    {
        return undef
            if ( $test );
        Error ("Make data file missing: $cfg_file. Rebuild required");
    }

    delete $INC{ $cfg_file };
    require $cfg_file;

    Error ("Makefile info2 not present")
        unless ( keys %::cf_info2 );
    
    Error ("Makefile info2 incorrect version. Rebuild required")
        unless ( exists $::cf_info2{version} && $::cf_info2{version} eq 1 );

    %{$tag_data{$dir}} = %::cf_info2;
    $tag_data{$dir}{config} = $cfg_file;
    %{$tag_data{$dir}{full}} = %::cf_info;

#DebugDumpData ("ReadMaketags", $tag_data{$dir});
    return \%tag_data;

}

#-------------------------------------------------------------------------------
# Function        : ExpandComposite
#
# Description     : Expand a command via composite command expansion
#                   A composite command may contain other composite commands
#                   Detect, as an error, recursive expansions
#
# Inputs          : $cmd    - command to expand
#
# Returns         : An array of commands
#
sub ExpandComposite
{
    my ($cmd) = @_;
    my @cmds;
    my @scmds = $cmd;
    my %seen;
    while ( @scmds )
    {
        my $cmd = shift @scmds;
        if ( exists $composite_commands{$cmd} )
        {

            Error ("Internal. Recursion in composite command definitions: $cmd")
                if ($seen{$cmd});

            @scmds = (@{$composite_commands{$cmd}}, @scmds);
            $seen{$cmd} = 1;
        }
        else
        {
            push @cmds, $cmd;
        }
    }

    return @cmds;
}


#-------------------------------------------------------------------------------
#   Documentation
#

=pod

=head1 NAME

jmake - JATS make support tool

=head1 SYNOPSIS

 Usage: jats make [options][targets][flags]

 Where options:
    -h, -h -h, -man     - Help messages with increasing verbosity
    -verbose            - Verbose operation
    -debug              - Debug operation
    -default=target     - Default 'target' if none is supplied

 Where flags are of the form Name=Value
    SHOWENV=1           - Show make environment
    LEAVETMP=1          - Leave temp working files
    NODEPEND=1          - Ignore dependency checking
    EXPERT=1            - Ignore dependency on makefiles
    OPTIONS=[opt]       - Maketime options [args,allargs,filter...]

 Valid targets include:
    all                 - build and install everything(p*)
    build               - build everything (pu)
    debug               - build all things for debug (pu)
    prod                - build all things for production (pu)
    install             - install public components (pu*)
    lint                - lints the source (assumes debug)(p)
    package             - build all packages (pu*)
    package-{set}       - build specific package (see below) (pu*)
    run_tests           - Run the tests specified in the makefile
    run_unit_tests      - Run the automatic unit tests specified in the makefile
    deploy              - Run the deployment scripts (p*)
    rebuild             - recursively rebuild makefiles
    depend              - construct the dependencies (u*)
    rmlitter            - remove build litter (core, tmp and err) (*)
    clean               - delete generate, obj, libraries and programs (p*)
    clobber             - delete everything which can be remade (p*)
    help                - A list of alias and platforms

      (u) undo target available (ie uninstall)
      (p) optional [platform_] prefix targets (ie XXX_build)
      (*) optional [_debug|_prod] postfix targets (ie clean_debug)


=head1 OPTIONS

=over 8

=item B<-help>

Print a brief help message and exits.

=item B<-help -help>

Print a detailed help message with an explanation for each option.

=item B<-man>

Prints the manual page and exits.

=item B<-verbose>

Increase the level of verbosity of the program execution

=item B<-debug>

Increase the debug output during program execution


=item B<-default=target>

This options specifies the default target if none is provided on the command
line. Used by the jats wrapper to simplify processing.

=back

=head1 TARGETS

Targets are described above.

Most targets support three modifiers. These can be used in conjunction with each
other.

=over 8

=item undo

Many target operation can be undone by prefixing the tarhet with 'un' ie:
uninstall.

=item platform prefix

Many target operations can be limited to one platform by prefixing the target
with a valid platform name and an underscore. ie WIN32_all

=item build postfix. prod or debug

Many targets operations can be limited toi either production of debug by
adding '_prod' or '_debug' to the target. ie 'install_debug'

=back

=head1 FLAGS

Flags are in the form TAG=value. This is a format that it used as they will be
passed directly to the underlying makefiles. The recognised flags are:

=over 8

=item B<SHOWENV=1>

This flag will force the 'Environment' to be displayed before commands are executed

=item B<LEAVETMP=1>

This flag will cause temp files, created by the build process, to notr be deleted.

=item B<NODEPEND=1>

This flag will supress dependency checking. Makefiles will not be created when
the makefile.pl is changed. Source files will not be scanned for header files.

=item B<EXPERT=1>

This flag will supress dependency checking between object files and the
generated makefile. This option can be used while test building a large build
when the makefile has been rebuilt but the user does not wish all the object
files to be rebuilt.

=item B<OPTIONS=list,list>

This flags passes a list of comma seperated options into the makefile. The exact
set of available options is target specific. Refer to the JATS manual.


=back