Subversion Repositories DevTools

Rev

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

########################################################################
# Copyright (c) VIX TECHNOLOGY (AUST) LTD
#
# Module name   : androidBuilder.pl
# Module type   : Makefile system
# Compiler(s)   : Perl
# Environment(s): jats
#
# Description   : This program is invoked by the JATS Makefile System
#                 to 'build' an Android project from an Eclipse based
#                 Android project. It does this by:
#                   Creating a build.xml file from the Eclispe files
#                   Injecting properties into the build
#                   Invoking ANT to perform the build 
#
#                 This process requires external tool - delivered in packages
#                 These are:
#                   ant         - Normally more recent than that installed 
#                                 on the build machines
#                   androidSdk  - From package rather than installed
#                                 This provides flexability when a new Sdk
#                                 is required
#
# Usage:        The utility is invoked in a controlled manner from a Jats
#               makefile. The call is generated by the Android Toolset
#               Arguments:
#                -verbose                   - Increase debugging
#                -verbose=n                 - Increase debugging
#                -f=manifestFile.xml        - project Manifest file
#                -i=path                    - Path to the interface directory
#                -t=[P|D]"                  - Build Type. Production or Debug
#                -pn=PackageName            - Package Name
#                -pv=PackageVersion         - Package Version
#                -clean                     - Will clean the build
#                -populate                  - Test Env and Populate 'libs'
#               Aguments that can be provided by the user
#                -projectname=ProjectName   - Project Name [optional]
#                -target=number             - Project Target
#                -Jar=name                  - Name of a Jar to include
#                -lname                     - Name of a Jats library to include
#                -Lname                     - Name of a 3rd party library to include
#
#......................................................................#

require 5.008_002;
use strict;
use warnings;

use Getopt::Long qw(:config pass_through);

use JatsError;
use JatsSystem;
use FileUtils;
use JatsProperties;
use JatsVersionUtils;
use ReadBuildConfig;
use JatsCopy;
use ArrayHashUtils;

#
#   Globals
#   Command line arguments
#
my $opt_verbose = $ENV{GBE_VERBOSE};
my $opt_manifestFile;
my $opt_interface;
my $opt_gbetype = 'P';
my $opt_clean;
my $opt_pkgname;
my $opt_pkgversion;
my $opt_projectname;
my $opt_populate;
my $opt_target = 1;
my @opt_jlibs;
my @opt_elibs;
my @opt_jars;

#
#   Configuration
#   Map JATS platforms to Shared library targets
#
my %SharedLibMap = (
    'ANDROIDARM'    => 'armeabi',
    'ANDROIDMIPS'   => 'mips',
    'ANDROIDX86'    => 'x86',
    );

#   Build type in ant format: release or debug
my $ant_target;

#-------------------------------------------------------------------------------
# Function        : Main Entry Point 
#
# Description     : Main entry to this program
#
# Inputs          : @ARGV           - Array of command line arguments
#                                     See file header
#
# Returns         : 0               - No Error
#                   1               - Error encountered    
#
InitFileUtils();
ErrorConfig( 'name'    => 'ANDROIDBUILDER',
             'verbose' => $opt_verbose);

#
#   Install local signal handlers to process GetOptions messages
#
local $SIG{__WARN__} = sub { ReportError('AndroidBuilder.' . "@_"); };
local $SIG{__DIE__} = sub { ReportError('AndroidBuilder.' . "@_"); };
my $result = GetOptions (
                "verbose:+"     => \$opt_verbose,       # flag
                "f=s"           => \$opt_manifestFile,  # string
                "i=s"           => \$opt_interface,     # Interface directory
                "t=s"           => \$opt_gbetype,       # string
                "pn=s"          => \$opt_pkgname,       # string
                "pv=s"          => \$opt_pkgversion,    # string
                "projectname=s" => \$opt_projectname,   # string
                "clean"         => \$opt_clean,         # flag
                "populate"      => \$opt_populate,      # flag
                "target:i"      => \$opt_target,        # Number
                "Jar=s"         => \@opt_jars,
                );

#
#   Restore signal handlers and report parse errors
#
$SIG{__WARN__} = 'DEFAULT';
$SIG{__DIE__} = 'DEFAULT';
Error('AndroidBuilder. Invalid call options detected') if (!$result);

#
#   Process remaining arguments
#   Only --Lname and --lname are valid
#
foreach my $arg (@ARGV) {
    if ($arg =~ m~^[-]{1,2}l(.*)~) {
        push @opt_jlibs, $1;
    } elsif ($arg =~ m~^[-]{1,2}L(.*)~) {
        push @opt_elibs, $1;
    } else {
        ReportError("Invalid option: $arg");
    }
}
ErrorDoExit();

#
#   Sanity Test
#
ReportError ("Manifest file not specified") unless ( defined $opt_manifestFile); 
ReportError ("Manifest file not found: $opt_manifestFile") unless ( -f $opt_manifestFile);

ReportError ("Interface directory not specified") unless ( defined $opt_interface);
ReportError ("Interface directory not found: $opt_interface") unless ( -d $opt_interface);

ReportError ("Package Name not specified") unless ( defined $opt_pkgname); 
ReportError ("Package Version not specified") unless ( defined $opt_pkgversion); 
ErrorDoExit();

#
#   Basic setup
#
$ant_target = $opt_gbetype eq 'P' ? 'release' : 'debug';

#
#   If multiple project are built in the same package it may be best to provide
#   a project name
#
$opt_projectname = $opt_pkgname
    unless (defined $opt_projectname);

#
#   The AndroidManifest.xml file MUST be in the root of the project
#   There will be some other files there too
#   Calculate the root of the project
#
my $project_root = StripFileExt($opt_manifestFile);
   $project_root = '.' unless $project_root;
my $project_buildfile = catfile($project_root, 'build.xml');

Message ("Project Base:" . Getcwd());
Message ("Project Root:" . $project_root);
Message ("Project Name:" . $opt_projectname);
Message ("Project build file:" . $project_buildfile);
Verbose ("Interface:" . $opt_interface);

#
#   Essential tool
#       ant     - setup ANT_HOME for other tools
#
#
ReadBuildConfig( $opt_interface, 'ANDROID', '--NoTest' );
my $antTool = getToolInfo('ant', 'JAVA_VERSION');
$ENV{ANT_HOME} = catdir($antTool->{PKGBASE}, $antTool->{TOOLROOT});

#
#   Setup the required version of Java for the tool
#
my $javaVersion = $antTool->{JAVA_VERSION};
ReportError ("$javaVersion not defined.", "Building ANDROID requires $javaVersion be installed and correctly configured.") 
    unless $ENV{$javaVersion};
$ENV{JAVA_HOME}=$ENV{$javaVersion};

#
#   Essential tool
#       androidSdk  - setup path to the android executable
#
my $androidSdk = getToolInfo('androidSdk');
my $androidPkg = catdir($androidSdk->{PKGBASE}, $androidSdk->{TOOLROOT} );
my $ANDROID = catdir($androidPkg,'sdk', 'tools', 'android' );
ReportError("Tool Package 'androidSdk' does not provide program 'android'")
    unless -f ($ANDROID);
ErrorDoExit();

#
#   Clean out any build artifacts
#
if ($opt_clean)
{
    #
    #   Invoke ANT on the build script - if present
    #
    Message ("Clean the existing build");
    JatsCmd('ant', '-buildfile', $project_buildfile, 'clean')
        if (-f $project_buildfile);
    deleteGeneratedFiles();
    exit 0;
}

#
#   Delete files that we will create and inject
#       Not sure about project.properties
#       It appears that it can be written - and info will be lost
#
deleteGeneratedFiles();

#
#   Populate the Android Project 'libs' directory
#
injectDependencies();

#
#   Create the project files
#       The android tool does not appear to provide an non-zero exit code on error
#       Detect error via the non-creation of the expected output file
#
System($ANDROID, $opt_verbose ? '--verbose' : '--silent',
                 'update', 'project', 
                 '--path', $project_root, 
                 '--subprojects', 
                 '--target', $opt_target, 
                 '--name', $opt_projectname );
unless ( -f catfile($project_root, 'build.xml'))
{
    Error("Cannot update android project: $opt_projectname");
}

#
#   The provided AndroidManifest.xml file contains 
#       android:versionCode and android:versionName
#   These prevent the ones in ant.properties from being reflected in the output
#   Rewrite the AndroidManifest.xml file with corrected versions
#
updateManifest();

#
#   If we are only populating the build then we have done all we need to do
#
if ($opt_populate)
{
    Verbose ("Populate complete");
    exit 0;
}

#
#   Build the Android project through the ANT package
#       ANT_HOME    - has been set up
#       JAVA_HOME   - has been set up
#
Message ("Build the android project: $opt_projectname");
my $rv = JatsCmd('ant', '-buildfile', $project_buildfile, $ant_target);
Error("Cannot build android project: $opt_projectname") if $rv;
exit(0);

#-------------------------------------------------------------------------------
# Function        : updateManifest 
#
# Description     : Calculate Version information
#                   Rewrite the Projects Manifest file and local properties files
#
# Inputs          : None 
#
# Returns         : Nothing
#
sub updateManifest
{
    #
    #   Generate Package Versioning information   
    #       Need a text string and a number
    #       Generate the 'number' from the version number
    #
    my $version_text;
    my $version_num;

    $version_text = $opt_pkgversion;
    my ($major, $minor, $patch, $build )= SplitVersion($opt_pkgversion);
    foreach my $item ($major, $minor, $patch, $build)
    {
        Error("Package version has invalid form. It contains non-numeric parts", $item)
            unless ( $item =~ m~^\d+$~);
    }
    $version_num = ($major << 24) + ($minor << 16) + ($patch << 8) + $build;

    Message ("Project Version Txt:" . $version_text);
    Message ("Project Version Num:" . $version_num);

    #
    #   Rewrite the Manifest File 
    #       Delete the Versioning information
    #       It will be picked up from the ant.properties file
    #       Store it in the interface directory
    #
    my $jats_androidManifestFile = catfile($opt_interface, $opt_projectname . '_'.'AndroidManifest.xml');
    Message ("Rewrite Manifest: " . $jats_androidManifestFile);

    open (AM, '<', $opt_manifestFile) or Error("Cannot open $opt_manifestFile: $!");
    open (JAM, '>', $jats_androidManifestFile) or Error ("Cannot create $jats_androidManifestFile: $!");
    while (<AM>)
    {
        s~(android:versionCode=").*(")~$1$version_num$2~;
        s~(android:versionName=").*(")~$1$version_text$2~;
        print JAM $_
    }
    close AM;
    close JAM;

    #
    #   Create the ant.properties file
    #   It needs to be in the same directory as the build.xml
    #
    my $antProperies = catfile($project_root,'ant.properties');
    Message ("Create Properties file: " . $antProperies);

    my $data = JatsProperties::New();

    #$data->setProperty('source.dir'        , 'src');

    #   Appears to work best when left as default
    #$data->setProperty('out.dir'           , 'bin');

    #   Don't write these to the properties file as it will
    #   create warning messages. The data is in the Manifest File
    #
    #$data->setProperty('version.code'      , $version_num);
    #$data->setProperty('version.name'      , $version_text);

    $data->setProperty('manifest.file'      , FullPath($jats_androidManifestFile));
    $data->setProperty('verbose'            , $opt_verbose ? 'true' : 'false');

    #   May be of interest
    #<property name="jar.libs.dir" value="libs" />
    #<property name="jar.libs.absolute.dir" location="${jar.libs.dir}" />
    #<property name="native.libs.absolute.dir" location="libs" />
    #
    #<property name="out.dir" value="bin" />
    #<property name="out.absolute.dir" location="${out.dir}" />
    #<property name="out.classes.absolute.dir" location="${out.dir}/classes" />
    #<property name="out.res.absolute.dir" location="${out.dir}/res" />
    #<property name="out.rs.obj.absolute.dir" location="${out.dir}/rsObj" />
    #<property name="out.rs.libs.absolute.dir" location="${out.dir}/rsLibs" />
    #<property name="out.aidl.absolute.dir" location="${out.dir}/aidl" />
    #<property name="out.dexed.absolute.dir" location="${out.dir}/dexedLibs" />
    #<property name="out.manifest.abs.file" location="${out.dir}/AndroidManifest.xml" />

    #
    #   Insert key information
    #   Only used in the 'release' build
    #   At the moment the signing step MUST be done outside of the build system
    #
    #$data->setProperty('key.store'               , 'vix-pcc.keystore');
    #$data->setProperty('key.alias'               , 'pcc');
    #$data->setProperty('key.store.password'      , 'VixPassword');
    #$data->setProperty('key.alias.password'      , 'VixPassword');

    $data->store( $antProperies );
}

#-------------------------------------------------------------------------------
# Function        : injectDependencies 
#
# Description     : Populate the 'libs' directory
#                       
#                   Inject dependencies
#                   The android build will make use of files in the 'libs' directory
#                   There are two types of files that can be placed in that directory
#                   These appear to be:
#                       1) .jar files
#                       2) Shared libraries provided by NDK components
#                   
#                   Assume that the user is doing the right thing and not manually placing
#                   external dependencies in the 'libs' directory
#                   
#                   Clean out all files and repopulate - depending on the build type
#                   It may be different for debug and production builds
#
# Inputs          : 
#
# Returns         : 
#
sub injectDependencies
{
    my @jlist;
    my @jpathlist;
    my @liblist;

    #
    #   Only if we need to do something
    #
    return unless (@opt_jars || @opt_elibs || @opt_jlibs);

    #
    #   Create search entries suitable for the CopyDir
    #   We will delete entries as the files are copied
    #   Allow for:
    #       jar files to have a .jar suffix (optional)
    #       jar files to have path specified with a package
    #
    foreach my $item ( @opt_jars) {
        $item =~ s~\.jar~~i;
        if ($item =~ m~/~)
        {
            UniquePush \@jpathlist, $item . '.jar';
        } else {
            UniquePush \@jlist, $item . '.jar';
        }
    }

    foreach my $item ( @opt_elibs) {
        UniquePush \@liblist, 'lib' . $item . '.so';
    }
    
    foreach my $item ( @opt_jlibs) {
        UniquePush \@liblist, 'lib' . $item . $opt_gbetype . '.so';
    }


    #
    #   Where does it go
    #
    my $androidLibs = catdir($project_root, 'libs');
    Verbose ("Android libs: $androidLibs");

    my @pkg_paths = getPackagePaths("--Interface=$opt_interface");
    foreach my $pkg ( @pkg_paths)
    {
        #
        #   Copy in all JAR files found in dependent packages
        #
        my $jarDir = catdir($pkg,'jar');
        if (-d $jarDir && @jlist)
        {
            Verbose("Jar Dir Found found", $jarDir);
            Message ("Copy in: $jarDir");
            CopyDir ( $jarDir, $androidLibs,
                        'Match' => \@jlist,
                        'Log' => $opt_verbose + 1,
                        'SymlinkFiles' => 1,
                        'Examine' => sub 
                            {
                                my ($opt) = @_;
                                ArrayDelete \@jlist, $opt->{file};
                                return 1;
                            },
                        );
        }

        #
        #   Copy in JARs specified by a full pathname
        #
        foreach my $file (@jpathlist) 
        {
            my $jarFile = catdir($pkg, $file);
            if (-f $jarFile)
            {
                Verbose("Jar File Found found", $jarDir);
                Message ("Copy in: $jarFile");
                CopyFile ( $jarFile, $androidLibs,
                            'Log' => $opt_verbose + 1,
                            'SymlinkFiles' => 1,
                         );
                ArrayDelete \@jpathlist, $file;
            }
        }
        
        #
        #   Build up the Shared Library structure as used by JNI
        #   Note: Only support current JATS format
        #   Copy in .so files and in to process massage the pathname so that
        #   it confirms to that expected by the Android Project
        #
        my $libDir = catdir($pkg,'lib');
        if (-d $libDir && @liblist)
        {
            Verbose("Lib Dir Found found", $libDir);
            Message ("Copy in: $libDir");
            CopyDir ( $libDir, $androidLibs,
                        'Match' => \@liblist,
                        'Log' => $opt_verbose + 1,
                        'SymlinkFiles' => 1,
                        'Examine' => sub 
                            { 
                                my ($opt) = @_;
                                foreach my $platform ( keys %SharedLibMap ) {
                                    my $replace = $SharedLibMap{$platform};
                                    if ($opt->{'target'} =~ s~/$platform/~/$replace/~)
                                    {
                                        ArrayDelete \@liblist, $opt->{file};
                                        return 1;
                                    }
                                }
                                return 0;
                            },
                        );
        }
    }

    #
    #   Report files that could not be located. They were deleted from the lists
    #   as they were processed
    #       These are Warnings in populate Mode
    #       and errors at build time
    #
    if (@jlist || @liblist || @jpathlist)
    {
        my $fn = $opt_populate ? \&Warning : \&Error; 
        &$fn("External dependencies not found:", @jlist, @jpathlist, @liblist);
    }
}

#-------------------------------------------------------------------------------
# Function        : deleteGeneratedFiles 
#
# Description     : Delete files that we generate
#
# Inputs          : 
#
# Returns         : 
#
sub deleteGeneratedFiles
{
    #
    #   Delete files that we will create
    #       Not sure about project.properties
    #       It appears that it can be written - and info will be lost
    #
    my @deleteList = qw(local.properties build.xml proguard-project.txt);
    foreach my $file (@deleteList)
    {
        Verbose ("Deleting $project_root/$file");
        unlink catfile($project_root, $file);
    }

    #
    #   Clean out the 'libs' directory
    #       Assume that its populated with 'external' dependencies
    #       Leave the directory - it may have been checked into version control
    #
    my $androidLibs = catdir($project_root, 'libs');
    foreach my $item (glob(catdir($androidLibs, '*'))) {
        RmDirTree($item);
    }
}