Subversion Repositories DevTools

Rev

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

########################################################################
# Copyright (c) VIX TECHNOLOGY (AUST) LTD
#
# Module name   : jats.sh
# Module type   : Makefile system
# Compiler(s)   : n/a
# Environment(s): jats
#
# Description   : Determine packages from an SBOM for escrow purposes
#                 For a given bom_id determine all used packages
#                 Create various bits of useful information
#                   Extract commands
#                   Build Order
#                   Dependency Info
#                   Bad Packages
#
#
#......................................................................#

require 5.006_001;
use strict;
use warnings;
use JatsEnv;
use JatsError;
use JatsSystem;
use JatsRmApi;
use DBI;
use Getopt::Long;
use Pod::Usage;                             # required for help support
use Storable qw (dclone);


#
#   Config Options
#
my $VERSION = "1.0.0";              # Update this
my $opt_help = 0;
my $opt_manual;
my $opt_verbose = $ENV{'GBE_VERBOSE'};      # Allow global verbose
my $opt_sbom_id;
my $opt_rtag_id;
my $opt_test = 0;
my $opt_patch = 1;
my $opt_extract;

#
#   Data Base Interface
#
my $RM_DB;
my $DM_DB;

#
#   Global variables
#
my %os_id_list;                 # os_id in the SBOM
my %os_env_list;                # OS Environments
my %pv_id;                      # Packages in the SBOM
my %Package;                    # Per Package information
my %Release;                    # Release information
my %Release_pvid;               # Release info
my @StrayPackages;              # Non-top level packages
my @create_list;                # List of files created
my $fpref = "sbom";             # Sbom Prefix
our $GBE_RM_URL;
our $GBE_DM_URL;

#
#   Constants, that should be variable
#
my $rm_base = "/dependencies.asp?pv_id=";
my $dm_base = "/OsDefault.asp?bom_id=BOMID&os_id=";

#
#   Build types. Should be populated from a table
#
my %BM_ID = (
    1 => "Solaris",
    2 => "Win32",
    3 => "Linux",
    4 => "Generic",
);

my %BSA_ID = (
    1 => "Jats Debug",
    2 => "Jats Prod",
    3 => "Jats Debug+Prod",
    4 => "Ant Java 1.4",
    5 => "Ant Java 1.5",
    6 => "Ant Java 1.6",
);

#
#   Packages to be ignored
#
my %ignore;
my %patch;


#-------------------------------------------------------------------------------
# Function        : Main
#
# Description     : Main entry point
#                   Parse user options
#
# Inputs          :
#
# Returns         :
#

my $result = GetOptions (
                "help+"         => \$opt_help,              # flag, multiple use allowed
                "manual"        => \$opt_manual,            # flag
                "verbose+"      => \$opt_verbose,           # flag
                "sbomid=s"      => \$opt_sbom_id,           # string
                "bom_id=s"      => \$opt_sbom_id,           # string
                "rtagid=s"      => \$opt_rtag_id,           # string
                "rtag_id=s"     => \$opt_rtag_id,           # string
                "ignore=s",     => sub{my ($a,$i) = @_; $ignore{$i} = 0 },
                "test!"         => \$opt_test,              #[no]flag
                "patch!"        => \$opt_patch,             #[no]flag
                "extract=s"     => \$opt_extract,           # Name of 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_manual || ($opt_help > 2));

ErrorConfig( 'name'    => 'ESCROW',
             'verbose' => $opt_verbose );

#
#   Sanity test
#
unless ( $opt_rtag_id || $opt_sbom_id || $opt_extract)
{
    Error ("Need sbomid and/or rtagid, or -extract",
           "Example: -sbomid=13543, for NZS Phase-1",
           "Example: -sbomid=13543 -rtagid=xxxx, for NZS Phase-1, comapred against given release",
           "Example: -rtagid=2362, for Sydney R1/R2",
    )
}

#
#   The extract option is special
#   It places the progam in a different mode
#
if ( $opt_extract )
{
    Error ("Cannot mix -extract with sbomid or rtagid" )
        if ( $opt_rtag_id || $opt_sbom_id );

    extract_files();
    exit (0);

}

Warning ("No sbomid provided. Output based an a Release") unless ( $opt_sbom_id );
$dm_base =~ s~BOMID~$opt_sbom_id~ if ($opt_sbom_id);
$fpref = "release" unless ( $opt_sbom_id );

#
#   Import essential EnvVars
#
EnvImport('GBE_RM_URL');
EnvImport('GBE_DM_URL');

$rm_base = $GBE_RM_URL . $rm_base;
$dm_base = $::GBE_DM_URL . $dm_base;

if ( $opt_sbom_id )
{
    #
    #   Determines the OS_ID's for the bom
    #
    getOSIDforBOMID($opt_sbom_id);

    #
    #   Locate packages associated with the base install for each os
    #
    foreach my $base_env_id ( sort keys %os_env_list )
    {
        getPackagesforBaseInstall( $base_env_id );
    }

    #
    #   Determine all the top level packages in the BOM
    #
    foreach my $os_id ( sort keys %os_id_list )
    {
        getPackages_by_osid( $os_id );
    }


    #
    #   For each Top Level Package determine the dependent packages
    #
    foreach my $pv_id ( keys %pv_id )
    {
        getPkgDetailsByPV_ID( $pv_id);
    }
    LocateStrays();

    #
    #   Determine packages in a given Release
    #
    if ( $opt_rtag_id )
    {
        getPkgDetailsByRTAG_ID( $opt_rtag_id );
    }
}
else
{
    getPkgDetailsByRTAG_ID( $opt_rtag_id );
my $count = 0;
    foreach my $pv_id ( keys %Release_pvid )
    {
next if ( $opt_test && ++$count > 3 );
        getPkgDetailsByPV_ID( $pv_id);
    }
    LocateStrays();
}


#
#   Remove packages to be ignored
#
foreach my $pkg ( keys %ignore )
{
    delete $Package{$pkg};
}

##
##   Display a list of all packages found so far
##
#foreach my $name ( sort keys %Package )
#{
#    foreach my $ver ( sort keys %{$Package{$name}} )
#    {
#
#        my $label = $Package{$name}{$ver}{label} || '';
#        my $path = $Package{$name}{$ver}{path} || '';
#
#        printf ("%30s %15s %45s %s\n", $name, $ver, $label, $path );
#    }
#}

#
#   Generate output files
#       1) Jats extract commands
#       2) Error list
my $file;
$file = "${fpref}_extract.txt";
push @create_list, $file;
open (JE, ">$file" ) || Error ("Cannot create $file");

$file = "${fpref}_status.txt";
push @create_list, $file;
open (ST, ">$file" ) || Error("Cannot create $file");
print ST "Cannot build:\n";

foreach my $name ( sort keys %Package )
{
    foreach my $ver ( sort keys %{$Package{$name}} )
    {

        my $label = $Package{$name}{$ver}{label} || '';
        my $path = $Package{$name}{$ver}{path} || '';
        my $mtest = exists ($Package{$name}{$ver}{build} ) || '0';
        my @reason1;            # can't extract files
        my @reason2;            # Others

        push @reason1, 'No Label' unless ( $label );
        push @reason1, 'Bad Label, N/A' if ( $label =~ s~^N/A$~~i || $label  =~ s~^na$~~i );

        push @reason1, 'No Source Path' unless ( $path );
        push @reason1, 'Bad Path, N/A' if ( $path =~ m~^N/A$~i || $path  =~ m~^na$~i );
        push @reason1, 'Bad Path, dpkg' if ( $path =~ m~^/dpkg_archive~ || $path  =~ m~^dpkg_archive~ );
        push @reason1, 'Bad Path, http' if ( $path =~ m~^http:~i );
        push @reason1, 'Bad Path, Drive' if ( $path =~ m~^[A-Za-z]\:~ );
        push @reason1, 'Bad Path, UNC' if ( $path =~ m~^//~ );
        push @reason1, 'Bad Path, Relative' unless ( $path =~ m~^/~ );


        push @reason2, 'No Build System' unless ( exists ($Package{$name}{$ver}{build} ) );

        unless ( @reason1 )
        {
            my $vname = "$name $ver";
            $vname =~ s~ ~_~g;
            $vname =~ s~__~~g;

            print JE "jats extract -extractfiles -view=$vname -label=$label -path=$path -root=. -noprefix\n";
        }

        if ( @reason1 || @reason2 )
        {
            $Package{$name}{$ver}{bad_extract} = [@reason1, @reason2];
            printf ST "%40s %20s %50s (%s) %s\n", $name, $ver, $label, $mtest, $path ;
        }
    }
}

close (JE);
close (ST);

#
#   Generate Leaf Package Information
#
LeafTracking();

#
#   Generate build order info
#
BuildOrder();

#
#   Generate HTML depenedancy information and other useful stuff
#
GenerateHTML();


#
#   Display names of files created
#
foreach my $file ( sort @create_list )
{
    Message ("Created: $file");
}
exit;


#-------------------------------------------------------------------------------
# Function        : getOSIDforBOMID
#
# Description     : Get all the os_id's associated with a BOMID
#
# Inputs          : $bom_id             - BOM to process
#
# Returns         :
#

sub getOSIDforBOMID
{
    my ($bom_id) = @_;
    my $foundDetails = 0;
    my (@row);

    connectDM(\$DM_DB) unless ($DM_DB);

    my $m_sqlstr = "SELECT distinct os.OS_ID, os.OS_NAME, nn.NODE_NAME, obe.BASE_ENV_ID " .
                   " FROM OPERATING_SYSTEMS os, BOM_CONTENTS bc, NETWORK_NODES nn, OS_BASE_ENV obe" .
                   " WHERE bc.BOM_ID = $bom_id AND bc.NODE_ID = os.NODE_ID AND nn.NODE_ID = os.NODE_ID AND obe.OS_ID = os.OS_ID ";

    my $sth = $DM_DB->prepare($m_sqlstr);
    if ( defined($sth) )
    {
        if ( $sth->execute( ) )
        {
            if ( $sth->rows )
            {
                while ( @row = $sth->fetchrow_array )
                {
                    Verbose ("OS_ID: ".join (',',@row) );
                    $os_id_list{$row[0]}{os_name} = $row[1];
                    $os_id_list{$row[0]}{node_name} = $row[2];

                    $os_env_list{$row[3]}{needed} = 1;
                    $os_env_list{$row[3]}{os_id}{$row[0]} = 1;
                }
            }
            $sth->finish();
        }
    }
    else
    {
        Error("getOSIDforBOMID:Prepare failure" );
    }
}

#-------------------------------------------------------------------------------
# Function        : getPackagesforBaseInstall
#
# Description     : Get all the packages for a given base install
#
# Inputs          :
#
# Returns         :
#

sub getPackagesforBaseInstall
{
    my ($base_env_id) =@_;
    my $foundDetails = 0;
    my (@row);

    connectDM(\$DM_DB) unless ($DM_DB);

    # First get details from pv_id

    my $m_sqlstr = "SELECT DISTINCT bec.PROD_ID, pkg.pkg_name, pv.pkg_version, pkg.pkg_id, pv.pv_id" .
                " FROM PACKAGES pkg, PACKAGE_VERSIONS pv,PRODUCT_DETAILS pd, BASE_ENV_CONTENTS bec".
                " WHERE bec.BASE_ENV_ID = $base_env_id AND bec.PROD_ID (+)= pv.PV_ID AND pv.pkg_id = pkg.pkg_id";

    my $sth = $DM_DB->prepare($m_sqlstr);
    if ( defined($sth) )
    {
        if ( $sth->execute( ) )
        {
            if ( $sth->rows )
            {
                while ( @row = $sth->fetchrow_array )
                {
                    Verbose ("OS ENV Package($base_env_id}:" . join (',',@row) );

                    my $pv_id =     $row[0];
                    my $name =      $row[1]  || 'BadName';
                    my $ver =       $row[2]  || 'BadVer';

                    $pv_id{$pv_id}{pkg_name} =$name;
                    $pv_id{$pv_id}{pkg_ver} = $ver;
                    foreach my $os_id ( keys %{$os_env_list{$base_env_id}{os_id}} )
                    {
                        $pv_id{$pv_id}{os_id}{$os_id} = 2;
                    }
                }
            }
            $sth->finish();
        }
        else
        {
            Error ("getPackagesforBaseInstall: Execute error");
        }
    }
    else
    {
        Error("getPackagesforBaseInstall:Prepare failure" );
    }

}


#-------------------------------------------------------------------------------
# Function        : getPackages_by_osid
#
# Description     : Get all the packages used by a given os_id
#
# Inputs          :
#
# Returns         :
#

my $count = 0;
sub getPackages_by_osid
{
    my ($os_id) =@_;
    my $foundDetails = 0;
    my (@row);

    connectDM(\$DM_DB) unless ($DM_DB);

    # First get details from pv_id

    my $m_sqlstr = "SELECT osc.*, pkg.pkg_name, pv.pkg_version, pd.IS_REJECTED, pv.IS_PATCH,pv.IS_OBSOLETE, pkg.pkg_id, pv.pv_id" .
                " FROM PACKAGES pkg, PACKAGE_VERSIONS pv,PRODUCT_DETAILS pd,".
                    "(" .
                        " SELECT osc.seq_num, osc.prod_id".
                        " FROM os_contents osc".
                        " WHERE osc.os_id = $os_id" .
                    " ) osc" .
                " WHERE pd.PROD_ID (+)= pv.PV_ID" .
                "   AND pv.pkg_id = pkg.pkg_id" .
                "   AND osc.PROD_ID = pv.pv_id" .
                " ORDER BY osc.SEQ_NUM desc" ;

    my $sth = $DM_DB->prepare($m_sqlstr);
    if ( defined($sth) )
    {
        if ( $sth->execute( ) )
        {
            if ( $sth->rows )
            {
                while ( @row = $sth->fetchrow_array )
                {
next if ( $opt_test && ++$count > 4 );
                    Verbose ("SBOM Package:".join (',',@row) );
                    my $pv_id =     $row[8];
                    my $name =      $row[2]  || 'BadName';
                    my $ver =       $row[3]  || 'BadVer';

                    $pv_id{$pv_id}{pkg_name} =$name;
                    $pv_id{$pv_id}{pkg_ver} = $ver;
                    $pv_id{$pv_id}{os_id}{$os_id} = 1;
                }
            }
            $sth->finish();
        }
    }
    else
    {
        Error("getPackages_by_osid:Prepare failure" );
    }
}

#-------------------------------------------------------------------------------
# Function        : getPkgDetailsByPV_ID
#
# Description     : Populate the Packages structure given a PV_ID
#                   Called for each package in the SBOM
#
# Inputs          : PV_ID           - Package Unique Identifier
#
# Returns         : Populates Package
#
sub getPkgDetailsByPV_ID
{
    my ($PV_ID) = @_;
    my $foundDetails = 0;
    my (@row);

    connectRM(\$RM_DB) unless ($RM_DB);

    # First get details from pv_id

    my $m_sqlstr = "SELECT pv.PV_ID, pkg.PKG_NAME, pv.PKG_VERSION, pv.PKG_LABEL, pv.SRC_PATH, pv.IS_DEPLOYABLE, pbi.BSA_ID, pbi.BM_ID" .
                    " FROM PACKAGE_VERSIONS pv, PACKAGES pkg, PACKAGE_BUILD_INFO pbi" .
                    " WHERE pv.PV_ID = \'$PV_ID\' AND pv.PKG_ID = pkg.PKG_ID AND pv.PV_ID = pbi.PV_ID (+) ";

    my $sth = $RM_DB->prepare($m_sqlstr);
    if ( defined($sth) )
    {
        if ( $sth->execute( ) )
        {
            if ( $sth->rows )
            {
                while ( @row = $sth->fetchrow_array )
                {
                    my $pv_id       = $row[0];
                    my $name        = $row[1];
                    my $ver         = $row[2];
                    my $label       = $row[3] || '';
                    my $path        = $row[4] || '';
                    my $deployable  = $row[5];
                    my $build_info  = $row[6] || '';
                    my $build_mach  = $row[7] || '';

                    #
                    #   BSA_ID: 1:debug, 2:prod, 3:debug+prod, 4:Java1.4 5: Java 1.5
                    #   BM_ID : 1:solaris, 2:win32, 3: linux, 4:generic
                    #


                    #
                    #   Does it look like a patch
                    #   We may want to ignore it.
                    #
                    my $patch = "";
                    unless ( $opt_patch )
                    {
                        if ( $ver =~ m~\.p\d+.\w+$~ )
                        {
                            $patch = "Patch";
                            $patch{$name} = 0
                                unless (  exists $patch{$name} );
                            $patch{$name}++;
                        }
                    }
                    Verbose ("getPkgDetailsByPV_ID: $PV_ID, $name, $ver, $build_mach ,$build_info, $patch");
                    next if ( $patch );


                    if ( exists $ignore{$name} )
                    {
                        Verbose2( "    Ignoring: $PV_ID, $name, $ver, $build_mach ,$build_info, $patch\n");
                        $ignore{$name}++;
                        last;
                    }

                    $path =~ tr~\\/~/~;                     # Don't compress multiples

                    $Package{$name}{$ver}{pvid} = $PV_ID;
                    $Package{$name}{$ver}{done} = 1;
                    $Package{$name}{$ver}{base} = 1;
                    $Package{$name}{$ver}{deployable} = 1 if ($deployable);
                    $Package{$name}{$ver}{label} = $label;
                    $Package{$name}{$ver}{path} = $path;
                    $Package{$name}{$ver}{build}{$build_mach} = $build_info if $build_mach;

                    GetDepends( $pv_id, $name, $ver );

                }
            }
            else
            {
                Warning ("No Package details for: PVID: $PV_ID");
            }
            $sth->finish();
        }
        else
        {
            Error("getPkgDetailsByPV_ID: Execute failure", $m_sqlstr );
        }
    }
    else
    {
        Error("Prepare failure" );
    }
}

#-------------------------------------------------------------------------------
# Function        : GetDepends
#
# Description     : Extract the dependancies for a given package version
#
# Inputs          : $pvid
#
# Returns         :
#
sub GetDepends
{
    my ($pv_id, $pname, $pver ) = @_;

    connectRM(\$RM_DB) unless ($RM_DB);

    #
    #   Now extract the package dependacies
    #
    my $m_sqlstr = "SELECT pkg.PKG_NAME, pv.PKG_VERSION, pd.DPV_ID" .
                   " FROM PACKAGE_DEPENDENCIES pd, PACKAGE_VERSIONS pv, PACKAGES pkg" .
                   " WHERE pd.PV_ID = \'$pv_id\' AND pd.DPV_ID = pv.PV_ID AND pv.PKG_ID = pkg.PKG_ID";
    my $sth = $RM_DB->prepare($m_sqlstr);
    if ( defined($sth) )
    {
        if ( $sth->execute( ) )
        {
            if ( $sth->rows )
            {
                my %depends;
                while ( my @row = $sth->fetchrow_array )
                {
#print "$pname $pver ===== @row\n";
                    my $name = $row[0];
                    my $ver = $row[1];

                    Verbose2( "       Depends: $name, $ver");

                    $depends{$name,$ver} = 1;
                    $Package{$name}{$ver}{usedby}{$pname,$pver} = 1;

                    unless ( exists $Package{$name}{$ver}{done} )
                    {
                        my @DATA = ($name, $ver, $row[2]);
                        push @StrayPackages, \@DATA;
                    }
                }
                $Package{$pname}{$pver}{depends} = \%depends;
            }
            $sth->finish();
        }
    }
    else
    {
        Error("GetDepends:Prepare failure" );
    }
}

#-------------------------------------------------------------------------------
# Function        : getPkgDetailsByRTAG_ID
#
# Description     : Extract all the packages for a given rtag_id
#
# Inputs          : RTAG_ID
#
# Returns         : 
#

sub getPkgDetailsByRTAG_ID
{
    my ($RTAG_ID) =@_;
    my $foundDetails = 0;
    my (@row);

    connectRM(\$RM_DB);

    # First get details from pv_id

    my $m_sqlstr = "SELECT pv.PV_ID, pkg.PKG_NAME, pv.PKG_VERSION".
                   " FROM RELEASE_CONTENT rc, PACKAGE_VERSIONS pv, PACKAGES pkg" .
                   " WHERE rc.RTAG_ID = $RTAG_ID AND rc.PV_ID = pv.PV_ID AND pv.PKG_ID = pkg.PKG_ID";
    my $sth = $RM_DB->prepare($m_sqlstr);
    if ( defined($sth) )
    {
        if ( $sth->execute( ) )
        {
            if ( $sth->rows )
            {
                while ( @row = $sth->fetchrow_array )
                {
                    my $pv_id   = $row[0];
                    my $name    = $row[1];
                    my $ver     = $row[2];
                    Verbose ("getPkgDetailsByRTAG_ID: $RTAG_ID, $name, $ver, $pv_id");

                    $Release{$name}{$ver}{pv_id} = $pv_id;
                    $Release_pvid{$pv_id} = 1;
                }
            }
            $sth->finish();
        }
    }
    else
    {
        Error("getPkgDetailsByRTAG_ID:Prepare failure" );
    }
}


#-------------------------------------------------------------------------------
# Function        : LocateStrays
#
# Description     : Locate stray packages
#                   These are packages that have not been defined by the
#                   top level SBOM. These are not really stray, although if the
#                   escrow is based on a RELEASE, then they will be packages
#                   from outside the release.
#
# Inputs          :
#
# Returns         :
#
sub LocateStrays
{
    while ( $#StrayPackages >= 0 )
    {
        my $DATA = pop @StrayPackages;
        my $name = $DATA->[0];
        my $ver = $DATA->[1];
        my $pv_id = $DATA->[2];

        next if ( exists $Package{$name}{$ver}{done} );
#print "Stray: $pv_id, $name, $ver\n";
        getPkgDetailsByPV_ID ( $pv_id );
        $Package{$name}{$ver}{stray} = 1;
    }
}

#-------------------------------------------------------------------------------
# Function        : BuildOrder
#
# Description     : Determine the order to build packages
#
# Inputs          :
#
# Returns         :
#
sub BuildOrder
{
    foreach my $name ( keys %Package )
    {
        foreach my $ver ( keys %{$Package{$name}} )
        {
            AddToBuildList( $name, $ver, $Package{$name}{$ver}{depends} );
        }
    }
    
    DetermineBuildOrder();
}

#-------------------------------------------------------------------------------
# Function        : AddToBuildList
#
# Description     : Add packages to a build list
#
# Inputs          : PackageName
#                   PackageVersion
#                   Hash of dependancies
#
# Returns         :
#
my %BuildList;
sub AddToBuildList
{
    my ($name, $ver, $pdepends ) = @_;

    Warning ("Duplicate Package to build: $name, $ver") if exists $BuildList{$name,$ver};

    #
    #   Clone dependancies as we will destroy the list as we process data
    #
    my $ref;
    $ref = dclone ($pdepends ) if $pdepends;
    $BuildList{$name,$ver}{depends} = $ref;
}

#-------------------------------------------------------------------------------
# Function        : DetermineBuildOrder
#
# Description     : Determine the build order
#
# Inputs          :
#
# Returns         :
#
sub DetermineBuildOrder
{

    my $file = "${fpref}_buildinfo.txt";
    push @create_list, $file;
    
    open (BI, ">$file" )  || Error ("Cannot create $file");

#    DebugDumpData ("BuildList", \%BuildList); exit 1;

    my $more = 1;
    my $level = 0;
    while ( $more )
    {
        my @build;
        $level ++;
        $more = 0;
        foreach my $key ( keys %BuildList )
        {
            #
            #   Locate packges with no dependencies left
            #
            next if ( keys %{$BuildList{$key}{depends}} );
            push @build, $key;
        }

        foreach my $build ( @build )
        {
            $more = 1;
            delete $BuildList{$build};
            my ($name, $ver) = split $;, $build;

            my $label = $Package{$name}{$ver}{label} || '';
            my $path  = $Package{$name}{$ver}{path} || '';
            $Package{$name}{$ver}{buildorder}  = $level;

            printf BI "Build(%2d): %40s %15s %-55s %s\n", $level, $name, $ver, $label, $path;
        }

        #
        #   Delete dependencies
        #
        foreach my $key ( keys %BuildList )
        {
            foreach my $build ( @build )
            {
                delete $BuildList{$key}{depends}->{$build};
            }
        }
    }
    close BI;
}

#-------------------------------------------------------------------------------
# Function        : GenerateHTML
#
# Description     : Generate Dependency information
#                   Generate a nive HTML dependancy table
#                   Shows DependOn and UsedBy
# Inputs          :
#
# Returns         :
#

sub GenerateHTML
{
    my $td = '<td style="vertical-align: top;">' . "\n";
    my $td3 = '<td style="vertical-align: top;" colspan="3">' . "\n";
    my $tdr = '<td style="text-align: right;">';

    my $file = "${fpref}_depends.html";
    push @create_list, $file;
    open (DP, ">$file" )  || Error ("Cannot create $file");

    #
    #   Generate an index
    #
    print DP "<dl><dt><h1>Index</h1></dt>\n";
    print DP "<dd><a href=\"#Ignore\">Ignored Packages</a></dd>\n";
    print DP "<dd><a href=\"#Depend\">Dependency Info</a></dd>\n";
    print DP "<dd><a href=\"#Multi\">Multiple Package Version</a></dd>\n";
    print DP "<dd><a href=\"#NoBuild\">Packages that cannot be built</a></dd>\n";
    print DP "<dd><a href=\"#Excess\">Excess Packages from Release: $opt_rtag_id</a></dd>\n" if ( $opt_rtag_id && $opt_sbom_id );
    print DP "<dd><a href=\"#Stray\">Required Packages, not part of the release</a></dd>\n" if ( $opt_rtag_id && !$opt_sbom_id );
    print DP "<dd><a href=\"#Inconsistent\">Packages in the Release, with inconsistent dependencies</a></dd>\n" if ( $opt_rtag_id && !$opt_sbom_id );

    print DP "</dl>\n";

    #
    #   Ignored Packages
    #
    print DP "<h1><a name=\"Ignore\">Ignored Packages</a></h1>\n";

    print DP "The following package, and all dependents, have been ignored.<br><br>\n";

    foreach my $name ( sort keys %ignore )
    {
        print DP "$name: $ignore{$name} versions<br>\n";
    }

    unless ( $opt_patch )
    {
        print DP "The following package have patches that have been ignored.<br><br>\n";
        foreach my $name ( sort keys %patch )
        {
            print DP "$name: $patch{$name} patches<br>\n";
        }
    }

    #
    #   Dependency Information
    #
    print DP "<h1><a name=\"Depend\">Dependency Info</a></h1>\n";

    print DP "<table border=\"1\"><tbody>\n";
    print DP "<tr>\n";
    print DP "<th>Package Dependency</th>\n";
    print DP "<th>Package Used by</th>\n";
    print DP "<th>Build Info</th>\n";
    print DP "</tr>\n";

    foreach my $name ( sort keys %Package )
    {
        foreach my $ver ( sort keys %{$Package{$name}} )
        {
            print DP "<tr>\n";
            #
            #   Depends On info
            #

            print DP $td;
            my $anchor= "${name}_${ver}";
            my $tag = "usedby_${name}_${ver}";
            printf  DP "<dl><dt><a name=\"$anchor\"></a><a href=\"#$tag\">%s&nbsp;%s</a> Depends on:</dt>\n", $name, $ver;
            foreach my $depend ( sort keys %{$Package{$name}{$ver}{depends}} )
            {
                my ($dname, $dver) = split $;, $depend;
                my $tag = "${dname}_${dver}";
                printf  DP "    <dd><a href=\"#$tag\">%s&nbsp;%s</a></dd>\n", $dname, $dver;
            }
            print DP "</dl>\n";
            print DP "</td>\n";


            #
            #   Used By information
            #
            print DP $td;
            $anchor= "usedby_${name}_${ver}";
            $tag = "${name}_${ver}";
            printf  DP "<dl><dt><a name=\"$anchor\"></a><a href=\"#$tag\">%s&nbsp;%s</a> Used by:</dt>\n", $name, $ver;
            foreach my $depend ( sort keys %{$Package{$name}{$ver}{usedby}} )
            {
                my ($dname, $dver) = split $;, $depend;
                my $tag = "usedby_${dname}_${dver}";
                printf  DP "    <dd><a href=\"#$tag\">%s&nbsp;%s</a></dd>\n", $dname, $dver;
            }
            print DP "</dl>\n";
            print DP "</td>\n";

            #
            #   Build Info
            #
            print DP $td;
            print DP "<table>";
            my $stray = ( exists ($Package{$name}{$ver}{stray}) && $Package{$name}{$ver}{stray} );

            my $pv_id = $Package{$name}{$ver}{pvid} || 'No PVID';
            my $pv_id_ref = $rm_base . $pv_id;
               $pv_id_ref .= "&rtag_id=" . $opt_rtag_id if ($opt_rtag_id && !$stray);
            my $pv_id_str = "<a href=\"$pv_id_ref\">$pv_id</a>";

            printf DP "<tr>${tdr}Pvid:</td><td>%s</td></tr>\n", $pv_id_str;
            printf DP "<tr>${tdr}Label:</td><td>%s</td></tr>\n", $Package{$name}{$ver}{label} || 'NoneProvided';
            printf DP "<tr>${tdr}Path:</td><td>%s</td></tr>\n", $Package{$name}{$ver}{path}  || 'NoneProvided';

            my $order = 'Not Built';
            my @machs;

            if ( exists($Package{$name}{$ver}{build}) )
            {
                $order = $Package{$name}{$ver}{buildorder};
                @machs = sort keys %{$Package{$name}{$ver}{build}};
            }
            else
            {
                my $tag = "notbuilt_${name}_${ver}";
                $order = "<a href=\"#$tag\">Not Built</a>"
            }

            printf DP "<tr>${tdr}Build Order:</td><td>%s</td></tr>\n", $order;

            my $text = "Build:";
            foreach my $mach ( @machs )
            {
                my $type = $Package{$name}{$ver}{build}{$mach};
                printf DP "<tr>${tdr}$text</td><td>%s&nbsp;%s</td></tr>\n", $BM_ID{$mach} || "Unknown, $mach", $BSA_ID{$type} || 'Unknown';
                $text = '';
            }

            my $pvid = $Package{$name}{$ver}{pvid};
            $text = "Deployed:";
            foreach my $osid ( sort keys %{ $pv_id{$pvid}{os_id}  } )
            {
                my $os_name = $os_id_list{$osid}{os_name};
                my $node =    $os_id_list{$osid}{node_name};

                my $ref = $dm_base . $osid;
                my $str = "<a href=\"$ref\">$node,($os_name)</a>";


                printf DP "<tr>${tdr}$text</td><td>$str</td></tr>\n";
                $text = '';
            }

            if ( $stray )
            {
                printf DP "<tr>${tdr}Stray:</td><td>Package included indirectly</td></tr>\n";
            }
            
            

            print DP "</table>";
            print DP "</td>\n";

            #
            #   End of Row
            #
            print DP "</tr>\n";
        }
    }
    print DP "</tbody></table>\n";


    #
    #   Multiple versions of a package
    #
    print DP "<h1><a name=\"Multi\">Multiple Package Versions</a></h1>\n";
    print DP "<table border=\"1\"><tbody>\n";
    print DP "<tr>\n";
    print DP "<th>Multiple Versions</th>\n";
    print DP "</tr>\n";

    foreach my $name ( sort keys %Package )
    {
        my @versions = keys %{$Package{$name}};
        next unless ( $#versions > 0 );
        print DP "<tr>\n";
        print DP $td;
        printf  DP "<dl><dt>$name</a> Versions:<dt>\n";

        foreach my $ver ( sort @versions )
        {
            my $tag = "${name}_${ver}";
            printf  DP "    <dd>";
            printf  DP "<a href=\"#$tag\">%s&nbsp;%s</a>\n", $name, $ver;
            print   DP " - Not in Release" if ($opt_rtag_id && $Package{$name}{$ver}{stray});
            printf  DP "</dd>\n", $name, $ver;
        }
        print DP "</dl>\n";
        print DP "</td>\n";
        print DP "</tr>\n";
    }
    print DP "</tbody></table>\n";


    #
    #   Packages that cannot be built
    #
    print DP "<h1><a name=\"NoBuild\">Packages that cannot be built</a></h1>\n";
    print DP "<table border=\"1\"><tbody>\n";
    print DP "<tr>\n";
    print DP "<th>Not Built</th>\n";
    print DP "</tr>\n";

    foreach my $name ( sort keys %Package )
    {
        my @versions = keys %{$Package{$name}};
        foreach my $ver ( sort @versions )
        {
            next unless exists($Package{$name}{$ver}{bad_extract});
            my @reasons = @{$Package{$name}{$ver}{bad_extract}};

            print DP "<tr><dl>\n";
            print DP $td;

            my $tag = "${name}_${ver}";
            my $anchor = "notbuilt_${name}_${ver}";
            
            printf  DP "<dt><a name=\"$anchor\"></a><a href=\"#$tag\">%s&nbsp;%s</a></dt>\n", $name, $ver;
            foreach my $reason ( @reasons )
            {
                print  DP "<dd>$reason</dd>\n";
            }


            print DP "</dl>\n";
            print DP "</td>\n";
            print DP "</tr>\n";
            
        }
    }
    print DP "</tbody></table>\n";

    #
    #   Packages that are in a specified release, but not described by the SBOM
    #
    if ( $opt_rtag_id && $opt_sbom_id )
    {
        print DP "<h1><a name=\"Excess\">Excess Packages from Release: $opt_rtag_id</a></h1>\n";
        print DP "<table border=\"1\"><tbody>\n";
        print DP "<tr>\n";
        print DP '<th colspan="3">Excess Packages</th>';
        print DP "</tr>\n";

        print DP "<tr>\n";
        print DP '<th>Package</th>';
        print DP '<th>PVID</th>';
        print DP '<th>Used Package</th>';
        print DP "</tr>\n";

        my $were_found = 0;
        my $not_found = 0;
        foreach my $name ( sort keys %Release )
        {
            my @versions = keys %{$Release{$name}};
            foreach my $ver ( sort @versions )
            {
                if (exists($Package{$name}{$ver}))
                {
                    $were_found++;
                    next;
                }
                $not_found++;

                print DP "<tr>\n";
                print DP $td;

                my $pv_id = $Release{$name}{$ver}{pv_id} || 'No PVID';
                my $pv_id_ref = $rm_base . $pv_id . "&rtag_id=" . $opt_rtag_id;
                my $pv_id_str = "<a href=\"$pv_id_ref\">$pv_id</a>";

                printf  DP "$name $ver ", $name, $ver;
                print DP "</td>\n";

                print DP $td;
                printf DP "Pvid: %s\n", $pv_id_str;
                print DP "</td>\n";

                print DP $td;
                my @pver = keys %{$Package{$name}};
                if (@pver)
                {
                    printf  DP "<dl><dt> Uses Versions:<dt>\n";
                    foreach my $ver ( sort @pver  )
                    {
                        my $tag = "${name}_${ver}";
                        printf  DP "    <dd><a href=\"#$tag\">%s&nbsp;%s</a></dd>\n", $name, $ver;
                    }
                    print DP "</dl>\n";
                }
                else
                {
                    printf DP "No Versions of this package used\n"
                }
                print DP "</td>\n";


                print DP "</tr>\n";
            }
        }

        print DP "<tr>\n";
        print DP $td3;
        print DP "Packages found in SBOM: $were_found";
        print DP "</td>\n";
        print DP "</tr>\n";

        print DP "<tr>\n";
        print DP $td3;
        print DP "Packages NOT found in SBOM: $not_found";
        print DP "</td>\n";
        print DP "</tr>\n";
        
        print DP "</tbody></table>\n";
    }

    #
    #   Packages that are strays
    #   They are not top level packages in the release
    #
    if ( $opt_rtag_id && !$opt_sbom_id )
    {
        print DP "<h1><a name=\"Stray\">Required Packages, not part of the release</a></h1>\n";
        print DP "<table border=\"1\"><tbody>\n";
        print DP "<tr>\n";
        print DP '<th colspan="3">Stray Packages</th>';
        print DP "</tr>\n";

        print DP "<tr>\n";
        print DP '<th>Inconsisient Package</th>';
        print DP '<th>PVID</th>';
        print DP '<th>Preferred Package</th>';
        print DP "</tr>\n";

        foreach my $name ( sort keys %Package )
        {

            my @versions = keys %{$Package{$name}};
            foreach my $ver ( sort @versions )
            {
                unless (exists($Package{$name}{$ver}{stray}) && $Package{$name}{$ver}{stray} )
                {
                    next;
                }

                #
                #   Determine preferred package version(s)
                #   These will be those without a 'stray' tag
                #
                my @preferred = ();
                foreach my $pver ( keys %{$Package{$name}} )
                {
                    next if (exists($Package{$name}{$pver}{stray} ) && $Package{$name}{$pver}{stray} );
                    push @preferred, $pver;
                }

                print DP "<tr>\n";

                #
                #  Package name and Used By information
                #
                print DP $td;
                my $anchor= "usedby_${name}_${ver}";
                my $tag = "${name}_${ver}";
                printf  DP "<dl><dt><a name=\"$anchor\"></a><a href=\"#$tag\">%s&nbsp;%s</a> Used by:</dt>\n", $name, $ver;
                foreach my $depend ( sort keys %{$Package{$name}{$ver}{usedby}} )
                {
                    my ($dname, $dver) = split $;, $depend;
                    my $tag = "usedby_${dname}_${dver}";
                    printf  DP "    <dd><a href=\"#$tag\">%s&nbsp;%s</a></dd>\n", $dname, $dver;
                }
                print DP "</dl>\n";
                print DP "</td>\n";


                my $pv_id = $Package{$name}{$ver}{pvid} || 'No PVID';

                my $pv_id_ref = $rm_base . $pv_id;
                my $pv_id_str = "<a href=\"$pv_id_ref\">$pv_id</a>";

                print DP $td;
                printf DP "Pvid: %s\n", $pv_id_str;
                print DP "</td>\n";

                #
                #   Insert Preferred package(s)
                #
                print DP $td;
                print DP "<table>\n";
                foreach my $pver ( sort @preferred )
                {
                    my $tag = "${name}_${pver}";
                    printf  DP "<tr><td><a href=\"#$tag\">%s&nbsp;%s</a></td></tr>\n", $name, $pver;
                }

                print DP "</table>\n";
                print DP "</tr>\n";
                
            }
        }

        print DP "</tbody></table>\n";
    }

    #
    #   Packages that have components not in the release
    #   They are not top level packages in the release
    #
    if ( $opt_rtag_id && !$opt_sbom_id )
    {
        print DP "<h1><a name=\"Inconsistent\">Packages in the Release, with inconsistent dependencies</a></h1>\n";
        print DP "<table border=\"1\"><tbody>\n";

        print DP "<tr>\n";
        print DP '<th>Inconsisient Package</th>';
        print DP "</tr>\n";

        foreach my $name ( sort keys %Package )
        {

            my @versions = keys %{$Package{$name}};
            foreach my $ver ( sort @versions )
            {
                #
                #   Ignore 'stray' packages
                #
                next if (exists($Package{$name}{$ver}{stray}) && $Package{$name}{$ver}{stray} );

                #
                #   Is it inconsitient
                #
                my $ok = 1;
                foreach my $depend ( sort keys %{$Package{$name}{$ver}{depends}} )
                {
                    my ($dname, $dver) = split $;, $depend;
                    if (exists($Package{$dname}{$dver}{stray}) && $Package{$dname}{$dver}{stray} )
                    {
                        $ok = 0;
                        last;
                    }
                }

                next if ( $ok );


                #
                #   Depends On info
                #

                print DP "<tr>\n";
                print DP $td;
                my $anchor= "${name}_${ver}";
                my $tag = "usedby_${name}_${ver}";
                printf  DP "<dl><dt><a name=\"$anchor\"></a><a href=\"#$tag\">%s&nbsp;%s</a> Inconsistent::</dt>\n", $name, $ver;
                foreach my $depend ( sort keys %{$Package{$name}{$ver}{depends}} )
                {
                    my ($dname, $dver) = split $;, $depend;
                    next unless (exists($Package{$dname}{$dver}{stray}) && $Package{$dname}{$dver}{stray} );
                    
                    my $tag = "${dname}_${dver}";
                    printf  DP "    <dd><a href=\"#$tag\">%s&nbsp;%s</a></dd>\n", $dname, $dver;
                }
                print DP "</dl>\n";
                print DP "</td>\n";
                print DP "<tr>\n";

            }
        }

        print DP "</tbody></table>\n";
    }
    
    


    close DP;
}

#-------------------------------------------------------------------------------
# Function        : LeafTracking
#
# Description     : Determine leaf packages and the base packages
#                   Create a CSV file containing leaf packages for
#                   each package in the SBOM/Release
#
# Inputs          : 
#
# Returns         : 
#
my %leaf;
sub LeafTracking
{
#    DebugDumpData ("Package", \%Package   );

    foreach my $name ( sort keys %Package )
    {

        my @versions = keys %{$Package{$name}};
        foreach my $ver ( sort @versions )
        {
            #
            #   Only process leaf packages
            #   These are packages that have no users
            #
            next if ( exists $Package{$name}{$ver}{usedby} );

            #
            #   For each leaf package/version
            #   Walk the dependancy tree and determine all of its dependents
            #
            my @plist = keys %{$Package{$name}{$ver}{depends}};
            my %done;
            while ( $#plist >= 0 )
            {
                my $depends = pop @plist;
                next if ( exists $done{$depends} );
                $done{$depends} = 1;

                my ($dname, $dver) = split $;, $depends;
                $leaf{$name}{$ver}{depends}{$depends} = 1;

                my @dlist = keys %{$Package{$dname}{$dver}{depends}};
                push @plist, @dlist;
            }
        }
    }

    #
    #   Now have a list of leaf packages and all dependent packages
    #   Invert this, and create a list of leaf packages for each package
    #
    foreach my $name ( keys %leaf )
    {
        foreach my $ver ( keys %{$leaf{$name}} )
        {
            foreach my $depends ( keys %{$leaf{$name}{$ver}{depends}}  )
            {
                my ($dname, $dver) = split $;, $depends;
                $Package{$dname}{$dver}{leaf}{$name,$ver} = 1;
            }
        }
    }

    #
    #   Put all the useful information into a CSV file
    #
    my $file = "${fpref}_affects.csv";
    push @create_list, $file;
    open (AF, ">$file" ) || Error("Cannot create $file");

    #
    #   Create CSV header
    #
    my @header = ('Package', 'Version', 'In Release', 'Buildable', 'Build Machine');

    push @header, $BM_ID{$_} foreach sort keys %BM_ID;
    push @header, 'Product List';

    print AF MakeCsv(@header),"\n";

    #
    #   Create Body of the data
    #
    foreach my $name ( sort keys %Package )
    {

        my @versions = keys %{$Package{$name}};
        foreach my $ver ( sort @versions )
        {
            #
            #   Only process leaf packages
            #   These are packages that have no users
            #
            next unless ( exists $Package{$name}{$ver}{leaf} );
            my @data = ($name, $ver);

            #
            #   Flag stray packages
            #
            my $stray = ( exists ($Package{$name}{$ver}{stray}) && $Package{$name}{$ver}{stray} );
            push @data, $stray ? 'N' : 'Y';

            #
            #   Flag packages that have duff data such that it cannot be build
            #
            my $bad = exists($Package{$name}{$ver}{bad_extract});
            push @data, $bad ? 'N' : 'Y';

            #
            #   Flag auto-buildable
            #   Really just a summary of the following fields. ie: build machine has been specified
            #
            my $buildable = exists($Package{$name}{$ver}{build});
            push @data, $buildable ? 'Y' : 'N';

            #
            #   Flag build machines
            #
            foreach my $bmid ( sort keys %BM_ID )
            {
                my $build = exists ($Package{$name}{$ver}{build}{$bmid} );
                push @data, $build ? 'Y' : 'N';
            }

            #
            #   Leaf packages
            #
            my @plist = sort keys %{$Package{$name}{$ver}{leaf}};
            foreach my $depends ( @plist )
            {
                my ($dname, $dver) = split $;, $depends;
                push @data, "$dname $dver";
            }

            #
            #   Print as a CSV entry
            #
            print AF MakeCsv(@data),"\n";
        }
    }
    close AF;

#    DebugDumpData ("Package", \%Package   );
#    DebugDumpData ("Leaf", \%leaf );
#    Error ("DONE");
}

sub MakeCsv
{
    my $string;
    my $pad = '';
    foreach  ( @_ )
    {
        next unless ( $_ );
        $string .= $pad . '"' . $_ . '"';
        $pad = ',';
    }
    return $string;
}

#-------------------------------------------------------------------------------
# Function        : extract_files
#
# Description     : Alternate mode of operation
#                   Extract files from the generated list. This is intended to
#                   be run as a sperate phase taking the 'extract' file
#
# Inputs          :
#
# Returns         : 
#
sub extract_files
{
    my @extract_order;
    my %extract;
    ErrorConfig( 'name'    => 'ESCROW-EXTRACT' );

    #
    #   Open the file and read in data in one hit
    #   This will detect file errors early
    #
    Error ("Cannot find specified file: $opt_extract")
        unless ( -f $opt_extract );

    open (FH, "<$opt_extract" ) || Error ("Cannot open file");
    while ( <FH> )
    {
        s~[\r\n]+$~~;
        next unless ( $_ );

        my ($view, $label, $path);
        foreach ( split ' ', $_ )
        {
            if ( m~^-view=(.+)~ ) {
                $view = $1;
            } elsif ( m~^-label=(.+)~ ) {
                $label = $1;
            } elsif ( m~^-path=(.+)~ ) {
                $path = $1;
            }
        }

        Error "Duplicate view name: $view" if ( exists $extract{$view} );
        Error "Bad file format in line: $_" unless ( $view && $label );
        push @extract_order, $view;
        $extract{$view}{label} = $label;
        $extract{$view}{path} = $path;
    }
    close FH;

    #
    #   Log the file processing
    #
    my $lfile = "${opt_extract}.log";
    open (FH, ">$lfile" ) || Error ("Cannot open log file: $lfile");

    #
    #   Process each entry
    #
    foreach my $view ( @extract_order )
    {
        my $label = $extract{$view}{label};
        my $path = $extract{$view}{path};
        my $rv = JatsCmd ("extract", "-extractfiles", "-view=$view", "-label=$label", "-path=$path", "-root=.", "-noprefix");
        print FH "$view : SUCCESS\n" unless $rv;
        print FH "$view : ERROR\n" if $rv;
    }
    close FH;

}


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

=pod

=head1 NAME

escrow - Extract Escrow Build Information

=head1 SYNOPSIS

  jats escrow [options]

 Options:
    -help              - brief help message
    -help -help        - Detailed help message
    -man               - Full documentation
    -sbomid=xxx        - Specify the SBOM to process
    -rtagid=xxx        - Specify the Release to process (Optional)
    -ignore=name       - Ignore packages with the specified name
    -extract=fname     - Extract files from a previous run
    -verbose           - Enable verbose output
    -[no]patch         - Ignore/Include patches. Default:Include
    -[no]test          - Reduced package scanning for test

=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<-sbomid=xxx>

This option is mandatory. It specifies the SBOM to process. The sbomid must be
determined from Deployment Manager.

=item B<-rtagid=xxx>

If provided, this option specifies an RTAG_ID to process in conjunction with the SBOM.
The RTAG_ID must be determined from Release Manager. The program will determine
packages that are in th Release, but not in the SBOM.

=item B<-ignore=name>

All versions of the named package will be ignored. This parameter is options.
It may be used multiple times.

=item B<-extract=name>

This option will process the 'extract' file created in a previous run of this
program and extract source files for the package-versions found in the file.

The command will then create a log file recording packages that could ne be
extracted.

This option cannot be used in conjunction wit the -rtagid or -sbomid.

=item B<-[no]patch>

This option is used ignore patches. If -nopatch is selected, then packages
versions that look like a patch will be added to the ignore list.

=item B<-[no]test>

This option is used for testing. It will only process the first two OS entries
in the SBOM. This speeds up processing. It does not generate a complete list of
packages.

=item B<verbose>

This option will display progress information as the program executes.

=back

=head1 DESCRIPTION

This program is a tool for extracting Escrow build information.

Given an SBOM_ID this program will:

=over 8

=item * Determine all the NODES in the SBOM

=item * Determine all the Base Packages for each NODE

=item * Determine all the Packages for each NODE

=item * Determine all the dependent packages for all packages encountered

=item * Generate a list of jats commands to extract the package source

=item * Generate a file describing the build order

=item * Generate a file describing the packages that cannot be built

=item * Generate a CSV file containing build information and leaf packages.

This file contains list of packages that are ultimately affected by the package.
This list does not include intermediate packages; only the deployed packages.

=item * Generate an HTML file with extensive cross reference information

=over 8

=item * List of all packages with references into Release Manager

=item * List of all packages showing dependent packages

=item * List of all packages showing consumer packages

=item * List of all packages for which multiple versions are required

=item * Details of packages that are not built.

=item * Build order

=item * Build machines and built types

=item * Deployed target nodes, with references into deployment manager

=back

=back

Given an RTAG_ID the program will do similar work, but the list of packages will be
limited to the one release.

This may take some time, as a typical escrow build may contain many hundreds of packages.

The program will display a list of files that have been created.

Given an 'extract' file from a previous run of this program the program will:

=over 8

=item * Parse the 'extract' file

=item * Create subdirectoroes for each package version within the file.

This is done in such a way that no views are left in place.

=item * Create a log file showing packages that could not be extracted.

=back

=cut