#! perl
########################################################################
# COPYRIGHT - VIX IP PTY LTD ("VIX"). ALL RIGHTS RESERVED.
#
# Module name   : jats_buildlinux.pl
# Module type   : Makefile system
# Compiler(s)   : n/a
# Environment(s): jats
#
# Description   : A utility to assist in building Linux Device Driver
#                 This script implemenst the runtime functionality of the
#                 MakeLinuxDriver directive
#
# Usage:
#
#......................................................................#

require 5.006_001;
use strict;
use warnings;
use Cwd;
use FileUtils;
use JatsSystem;
use JatsError;
use ArrayHashUtils;
use File::Path;
use File::Copy;
use Getopt::Long;
use Pod::Usage;                             # required for help support

#
#   Initialise and configure some of the used packages
#
ErrorConfig( 'name' => 'BuildLinuxDriver' );
SystemConfig ( 'ExitOnError' => 1);
InitFileUtils();

#
#   Global variables
#
my $VERSION = "1.0.0";                      # Update this
my $opt_debug   = $ENV{'GBE_DEBUG'};        # Allow global debug
my $opt_verbose = $ENV{'GBE_VERBOSE'};      # Allow global verbose
my $opt_help = 0;
my $opt_manual = 0;
my $opt_output;
my $opt_kernel;
my $opt_gcc_path;
my $opt_arch;
my @opt_source;
my $opt_driver;
my $opt_leavetmp;
my $opt_show_cmds;
my $opt_type = 'P';
my $opt_platform;
my @opt_defs;
my @opt_incdirs;
my $opt_localbindir;
my @opt_external_modules;
my @opt_external_modules_paths;
my $opt_clean;

#
#   Extract arguments
#
my $result = GetOptions (
                "help+"             => \$opt_help,              # flag, multiple use allowed
                "manual"            => \$opt_manual,            # flag, multiple use allowed
                "verbose+"          => \$opt_verbose,           # flag, multiple use allowed
                "Output=s"          => \$opt_output,
                "GccPath=s"         => \$opt_gcc_path,
                "Arch=s"            => \$opt_arch,
                "Driver=s"          => \$opt_driver,
                "LeaveTmp:s"        => \$opt_leavetmp,
                "Verbose:s"         => \$opt_show_cmds,
                "Type=s"            => \$opt_type,
                "Platform=s"        => \$opt_platform,
                "Define=s"          => \@opt_defs,
                "Define=s"          => \@opt_defs,
                "Incdir=s"          => \@opt_incdirs,
                "LocalBinDir=s"     => \$opt_localbindir,
                "ExternalModule=s"  => \@opt_external_modules,
                "Clean"             => \$opt_clean,
                );

#
#   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_manual || ($opt_help > 2));
#pod2usage(-verbose => 0, -message => "Version: $VERSION") if ( $#ARGV >= 0 );

#
#   Sanity Test user input
#
Error ("No Output File specified") unless ( $opt_output );

#
#   Locate the Kernel Headers
#   This will be provided by one of the external packages
#   Paths to the packages will be in @opt_incdirs
#   Look for a dir ectory with a 'Makefile' - use the first one
#
my $KERNELDIR;
foreach  ( @opt_incdirs )
{
    my $test_dir = $_ . '/' . $opt_platform;
    next unless ( -d $test_dir );
    next unless ( -f $test_dir . '/Makefile' );
    $KERNELDIR = $test_dir;
}
Error ("Could not find the Kernel Headers",
       "Need a package that contains include/$opt_platform/Makefile"
      ) unless ( $KERNELDIR );
#
#   Must have a nice name for the driver
#
Error ("No driver name specified") unless ( $opt_driver );

#
#   Calculate required bits
#   The generated file. Based on the name of the desired output file and the
#   source path.
#
my $gen_file = StripDir($opt_output);
my $out_dir = StripFileExt($opt_output);
my $symdir = catdir($opt_localbindir, $opt_driver);

#
#   Cross-compiler toolchain root
#   Based on GccPath, remove the gcc bit
#
my $CROSS_COMPILE = $opt_gcc_path;
   $CROSS_COMPILE =~ s~-gcc$~-~;

#
#   The required architecture
#
my $ARCH;
   $ARCH = 'arm'  if ( $opt_arch =~ m/arm/i );
   $ARCH = 'i386' if ( $opt_arch =~ m/i386/i );
   $ARCH = 'powerpc' if ( $opt_arch =~ m/powerpc/i );
   Error ("Cannot determine architecture") unless ( $ARCH );

#
#   Source files
#   These may be comma seperated. Expand the array
#
@opt_source = @ARGV;
Error ("No source files specified" ) unless ( @opt_source );

#
#   Locate required symvers files
#
if (@opt_external_modules)
{
    foreach my $emodule (@opt_external_modules)
    {
        my $mpath = catfile($opt_localbindir,$emodule,'Module.symvers');
        ReportError("External Module symbols not found for: " . $emodule) unless ((-f $mpath) || $opt_clean); 
        push @opt_external_modules_paths, $mpath;
    }
    ErrorDoExit() unless $opt_clean;
}

unless ($opt_clean)
{
    Message    "======================================================================";
    Message    "Build Linux Device Driver using the native build method";
    Message    "         Driver : $opt_driver";
    Message    "          Arch  : $ARCH";
    Message    "          Type  : $opt_type";
    Message    "         Output : $opt_output";
    Message    "       Platform : $opt_platform";
    Message    " Cross Compiler : $CROSS_COMPILE";
    Message    " Kernel Headers : $KERNELDIR";
    Message    "         Source : @opt_source";
    foreach  ( @opt_external_modules )
    {
        Message    "External Module : $_";
    }
    foreach  ( @opt_defs )
    {
        Message    "         Define : $_";
    }
    foreach  ( @opt_incdirs )
    {
        Message    "         Incdir : $_";
    }

    Message    "======================================================================";
}

#
#   Convert the list of source files into object files
#   Needed for cleaning
#
my @obj_files = map { $_ =~ s~\.\w+$~.o~; $_ } @opt_source;

#
#   Clean any existing build artifacts
#   The build options to place things in a different diractory are broken.
#   We must be able to build for multiple targets without confusion so we
#   clean, then build, then copy out the bits we need.
cleanAll();
exit if $opt_clean;

#
#   Generate a Makefile for use by the driver Builder
#   This encapsulates the complete driver build mechanism
#
create_makefile();

#
#   Remove some 'make' environment variables
#   The JATS make and the PACKAGE make must not know about each other
#
foreach my $var (qw ( MAKE MAKEFLAGS MAKEOVERRIDES MAKELEVEL MAKE_MODE
                     CC CXX CPP EXTRA_CFLAGS CFLAGS ASFLAGS LDFLAGS
                     DEBFLAGS LDDINC DEBUG KERNELRELEASE LDDINC
                     ))
{
    delete $ENV{$var};
}

#
#   Build up the make command
#   Create an array of arguments - Don't need to escape funny bits
#
my @make_args = ();
push @make_args, "ARCH=$ARCH";
push @make_args, "CROSS_COMPILE=$CROSS_COMPILE";
push @make_args, "KERNELDIR=$KERNELDIR";
push @make_args, "-f", "Kbuild";
push @make_args, "V=1" unless ( $opt_show_cmds );

System ( 'make', @make_args );

Error  ("Generated output module file not found: $gen_file") unless ( -f $gen_file );
copy ( $gen_file, $opt_output ) || Error ("Cannot copy output file: $opt_output");

#
#   Copy Module.symvers into a local directory for inter-module building
#   So far this is only supported within the same build - ie: Module.symvers
#   must be created in this build. The file will not be sourced from a package.
#   #
my $symfile = 'Module.symvers';
if ( -f $symfile && -s $symfile  )
{
    mkpath ($symdir) || Error ("Can't create $symdir", $!); 
    copy ( $symfile, $out_dir ) || Error ("Cannot copy $symfile to $out_dir");
    copy ( $symfile, $symdir ) || Error ("Cannot copy $symfile to $symdir");
}
clean() unless $opt_leavetmp;

Message "Script complete";
exit 0;

#-------------------------------------------------------------------------------
# Function        : create_makefile
#
# Description     : Generate a makefile called Kbuild
#                   This will be used by the kernel builder to do the hard work
#
#                   The makefile is created via a template embedded in this file
#
# Inputs          : None
#
# Returns         : Creates a file called Kbuild
#
sub create_makefile
{
    #
    #   Create variables for the data that will be substituted
    #
    my $driver_line = "obj-m	:= $opt_driver.o";

    my $file_list = "${opt_driver}-objs := @obj_files";
    my $debug_line = "DEBUG = y";
       $debug_line = "# " . $debug_line unless ( $opt_type eq 'D' );


    my $filename = "Kbuild";
    unlink ($filename);
    open (KBUILD, ">", $filename ) || Error ("Cannot generate file: $filename");
    while ( <DATA> )
    {
        $_ =~ s~\s+$~~;

        #
        #   Replace the entire line
        #
        $_ = $debug_line if ( m~__DEBUG_LINE__~ );
        $_ = $driver_line if ( m~__DEVICE_NAME__~ );
        $_ = $file_list if ( m~__FILE_LIST__~ );
        last if ( m~__END__~ );

        if ( m~__DEFINES__~ )
        {
            print KBUILD "# User definitions\n";
            foreach my $line ( @opt_defs )
            {
                print KBUILD "EXTRA_CFLAGS += -D$line\n";
            }
            print KBUILD "# End of User definitions\n";
            next;
        }

        if ( m~__EXTERNAL_MODULES__~ )
        {
            print KBUILD "# Start of KBUILD_EXTRA_SYMBOLS\n";
            foreach my $line ( @opt_external_modules_paths )
            {
                if ( -s $line)
                {
                    print KBUILD "KBUILD_EXTRA_SYMBOLS += \$(PWD)/$line\n";
                }
            }
            print KBUILD "# End of KBUILD_EXTRA_SYMBOLS\n";
            next;
        }

        if ( m~__INCDIR__~ )
        {
            print KBUILD "# Search Include Directories\n";
            foreach my $line ( @opt_incdirs )
            {
                if ( ! -d $line )
                {
                    Verbose("Incdir does not exist: $line");
                    next;
                }

                if ( $line =~ m~^/~ )
                {
                    print KBUILD "EXTRA_CFLAGS += -I$line\n";
                }
                else
                {
                    print KBUILD "EXTRA_CFLAGS += -I\$(PWD)/$line\n";
                }
            }
            print KBUILD "# End of User Include Directories\n";
            next;
        }
        

        print KBUILD "$_\n";
    }

    close (KBUILD);

}

#-------------------------------------------------------------------------------
# Function        : clean
#
# Description     : Clean up build artifacts
#                   Need to remove stuff that we build as the the kernel builder
#                   doesn't handle multiple platforms
#
# Inputs          : Globals: @obj_files
#
# Returns         : Nothing
#
sub clean
{
    foreach my $files (qw (*.o *~ core .depend .*.cmd *.ko *.mod.c Kbuild Module.symvers Module.markers modules.order ), @obj_files )
    {
        unlink ( glob ( $files ));
    }
    rmtree  ( '.tmp_versions' );
}

#-------------------------------------------------------------------------------
# Function        : cleanAll
#
# Description     : Clobber all build artifacts
#
# Inputs          : Globals: @obj_files
#
# Returns         : Nothing
#
sub cleanAll
{
    clean();
    rmtree  ( $symdir );
}


__DATA__
#
#   This is a generated file
#   Do not edit or check into version control
################################################################################

# Comment/uncomment the following line to disable/enable debugging
DEBUG = y __DEBUG_LINE__

__DEFINES__

__EXTERNAL_MODULES__

__INCDIR__

# Add your debugging flag (or not) to EXTRA_CFLAGS
ifeq ($(DEBUG),y)
  DEBFLAGS = -O -g -DDEV_DEBUG # "-O" is needed to expand inlines
else
  DEBFLAGS = -O2
endif

EXTRA_CFLAGS += $(DEBFLAGS)
EXTRA_CFLAGS += -I$(LDDINC)

ifneq ($(KERNELRELEASE),)
# call from kernel build system

llcd-objs := __FILE_LIST__
obj-m	:= __DEVICE_NAME__

else

KERNELDIR ?= "-- Must be defined by calling makefile"
PWD       := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) LDDINC=$(PWD)/../include modules

endif

depend .depend dep:
	$(CC) $(EXTRA_CFLAGS) -M *.c > .depend


ifeq (.depend,$(wildcard .depend))
include .depend
endif

__END__

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

=pod

=for htmltoc    MAKEUTIL::

=head1 NAME

jats_buildlinux - Build a Linux Device Driver

=head1 SYNOPSIS

perl jats_buildlinux [options] source_files+

 Options:               
    -help                 - brief help message
    -help -help           - Detailed help message
    -man                  - Full documentation
    -Output=path          - Path to the output file
    -Kernel=path          - Path to the Kernel Headers
    -GccPath=path         - Path to the gcc compiler to use
    -Arch=name            - Architecture name
    -Driver=name          - Base name of the driver to build
    -LeaveTmp=num         - Leave tmp files after build
    -Verbose=num          - Verbose command output
    -Type=[P|D]           - Type of build
    -Platform=name        - Target Platform
    -Define=text          - Definitions to pass to compiler
    -Incdir=path          - Extend the header file search path
    -LocalBinDir=path     - Path to store Module.symvers
    -ExternalModule=name  - Name of an external module
    -Clean                - Clean up generated files

=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.

=back

=head1 DESCRIPTION

This program is used internally by Jats to implement the body of the MakeLinuxDriver
directive. The command is not intended to be called by the user.

=head1 EXAMPLE

    MakeLinuxDriver ('*', 'llcd.ko',
                          '--KernelPackage=linux_kernel_headers', @CSRCS );
 

=cut

