Radix cross Linux Build System

Cross-platform build system is designed to build distributions of different operating systems for a set of target devices

39 Commits   2 Branches   2 Tags
#!/bin/env perl

use FindBin;
use lib $FindBin::Bin;

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

use IO::Handle;
use File::Basename;
use File::Temp;

use Time::localtime;

use _kxLab;

#
# Generate .$(HARDWARE).{json,html} file for current directory
#
# usage:
#   $0 topdir toolchain hardware
#
# where:
#      'topdir' - is a absolute path to the top directory of checked out branch
#   'toolchain' - is a TOOLCHAIN name
#    'hardware' - is a HARDWARE variant
#

# global variables
my ($build_system);
my ($topdir, $toolchain, $hardware, $flavour);
my ($target_build_dir, $requires_file);
my ($html_tree_file, $js_tree_file, $js_min_tree_file, $js_pkgs_file, $js_min_pkgs_file);
my ($system_version, $distro_name, $distro_version, $url);
my $tarball_suffix = "txz";

my %sub_trees;
my %tree;


sub usage
{
  print <<EOF;

Usage: $0 topdir toolchain hardware
Where:
          topdir - is a absolute path to the top of checked out branch;
       toolchain - is a TOOLCHAIN name;
        hardware - is a HARDWARE variant.

EOF
  exit;
}

#
# Getting information from build-system/constants.mk
#
sub system_version
{
  my $build_system = shift;
  my $version;

  open( FILE, "< $build_system/constants.mk" );

  while( <FILE> )
  {
    if( /^SYSTEM_VERSION(.+= +)(.+)/ )
    {
      $version = $2;
    }
  }
  close( FILE );

  return $version;
}

sub distro_name
{
  my $build_system = shift;
  my $name;

  open( FILE, "< $build_system/constants.mk" );

  while( <FILE> )
  {
    if( /^DISTRO_NAME(.+= +)(.+)/ )
    {
      $name = $2;
    }
  }
  close( FILE );

  return $name;
}

sub distro_version
{
  my $build_system = shift;
  my $name;

  open( FILE, "< $build_system/constants.mk" );

  while( <FILE> )
  {
    if( /^DISTRO_VERSION(.+= +)(.+)/ )
    {
      $name = $2;
    }
  }
  close( FILE );

  return $name;
}

sub bug_url
{
  my $build_system = shift;
  my $url;

  open( FILE, "< $build_system/constants.mk" );

  while( <FILE> )
  {
    if( /^BUG_URL(.+= +)(.+)/ )
    {
      $url = $2;
    }
  }
  close( FILE );

  return $url;
}

#
# value_from_makefile( $makefile, $variable_name, $stop_line ):
# ------------------------------------------------------------
#   This function returns the value of variable defined in the
#   Makefile on global level before od REQUIRES declaration.
#
#   The optional $stop_line argument presents the start of the
#   line which terminates the analized part of Makefile.
#
sub value_from_makefile
{
  my $makefile  = shift;
  my $vname     = shift;
  my $stop_line = shift;
  my $value     = "";

  if( !defined $stop_line || $stop_line eq '' )
  {
    $stop_line = "__END_OF_REQUIRES__";
  }
  else
  {
    $stop_line = "^" . $stop_line;
  }

  my $cdir = dirname( $makefile );

  my $shell_output = <<`SHELL`;
cd $cdir
head -n `cat Makefile | grep -n "$stop_line" | cut -f 1 -d ':'` Makefile | \
  make TOOLCHAIN=$toolchain HARDWARE=$hardware FLAVOUR=$flavour -f - -p __build_requires__ 2>/dev/null | grep "$vname"
exit 0
SHELL

  if( $shell_output =~ m/^$vname(.+= +)(.+)/gm )
  {
    $value = $2;
  }

  return $value;
}

#
# Getting information from Makefile
#
sub pkg_rootfs_target
{
  my $makefile = shift;
  my $install = "";

  open( FILE, "< $makefile" );

  while( <FILE> )
  {
    if( /^ROOTFS_TARGETS(.+= +)(.+)/ )
    {
      if( $2 ne "" ) { $install = "install"; }
    }
    elsif( /^ROOTFS_UPDATE_TARGETS(.+= +)(.+)/ )
    {
      if( $2 ne "" ) { $install = "update"; }
    }
  }
  close( FILE );

  if( $install eq "" ) { $install = "no";  }
#  else                 { $install = "yes"; }

  return $install;
}

sub pkg_group
{
  my $makefile = shift;
  my $group;

  open( FILE, "< $makefile" );

  while( <FILE> )
  {
    if( /^PKG_GROUP(.+= +)(.+)/ )
    {
      $group = $2;
    }
  }
  close( FILE );

  return $group;
}

sub pkg_name
{
  my $makefile = shift;
  my $name = "";

  open( FILE, "< $makefile" );

  while( <FILE> )
  {
    if( /^[A-Z_0-9]*_PKG_NAME(.+= +)(.+)/ )
    {
      $name = $2;
    }
  }
  close( FILE );

  return $name;
}

sub pkg_version
{
  my $makefile = shift;
  my $version;

  my $stop_line;

  open( FILE, "< $makefile" );

  while( <FILE> )
  {
    if( /^([A-Z_0-9]*_PKG_VERSION)(.+= +)(.+)/ )
    {
      $stop_line  = $1;
      $version    = $3;
    }
  }
  close( FILE );

  # check if version is present as a reference to some variable:
  if( $version =~ m/^\$\((.+)\)/ )
  {
    my $vname = $1;

    # get value of referenced variable
    $version = value_from_makefile( $makefile, $vname, $stop_line );
  }

  return $version;
}

sub pkg_license
{
  my $makefile = shift;
  my $license;

  open( FILE, "< $makefile" );

  while( <FILE> )
  {
    if( /^[A-Z_0-9]*_PKG_LICENSE(.+= +)(.+)/ )
    {
      $license = $2;
    }
  }
  close( FILE );

  return $license;
}

sub pkg_short_description
{
  my $makefile = shift;
  my $description;

  open( FILE, "< $makefile" );

  while( <FILE> )
  {
    if( /^[A-Z_0-9]*_PKG_SHORT_DESCRIPTION(.+= +)(.+)/ )
    {
      $description = $2;
    }
  }
  close( FILE );

  #
  # In Makefiles we have to mask characters '\', '&', '*', '(', ')' inside
  # the new value in the assignment operator with backslash. So, for axample,
  # the value "name & \ * ( ) end" we have to assign as follow
  #
  # ..._SHORT_DESCRIPTION = name \& \\ \* \( \) end
  #
  # Here we have to remove backslashes and fill escaped symbols as is:
  #
  $description =~ s/\\(.?)/$1/g;

  return $description;
}

#
# Getting information from tarball/{.PKGINFO | .DESCRIPTION}
#
sub get_pkg_info
{
  my $infofile   = shift;
  my $data_field = shift;
  my $data       = "";

  open( FILE, "< $infofile" );

  while( <FILE> )
  {
    if( /^$data_field=(.+)/ )
    {
      $data = $1;
    }
  }
  close( FILE );

  return $data;
}

sub get_pkg_description
{
  my $descfile   = shift;
  my $data       = "";
  my @fields;

  open( FILE, "< $descfile" );

  # Read the first line only
  @fields = split( ':', <FILE> );
  $data = $fields[1];
  $data =~ s/^\s+|\s+$//;
  $data =~ s/\"/\'/g;
  chomp $data;

  close( FILE );

  return $data;
}


sub get_treedirs
{
  my @list;

  seek( REQUIRES_FILE, 0, SEEK_SET );

  while( <REQUIRES_FILE> )
  {
    if( /^TREEDIRS(.+= +)(.+)/ )
    {
      @list = split( ' ', $2 );
    }
  }

  return @list;
}

sub get_ntreedirs
{
  my @list;

  seek( REQUIRES_FILE, 0, SEEK_SET );

  while( <REQUIRES_FILE> )
  {
    if( /^TREEDIRS(.+= +)(.+)/ )
    {
      @list = split( ' ', $2 );
    }
  }

  return $#list;
}

sub get_root
{
  my $root;

  seek( REQUIRES_FILE, 0, SEEK_SET );

  while( <REQUIRES_FILE> )
  {
    if( /^# ROOT(=)(.+)/ )
    {
      $root = $2;
    }
  }

  return $root;
}

sub get_deps
{
  my %deps;

  seek( REQUIRES_FILE, 0, SEEK_SET );

  while( <REQUIRES_FILE> )
  {
    if( /(.+)(: +)(.+)/ )
    {
      $deps{$1} = $3;
    }
  }
  return %deps;
}

my $root_node = 1;

#
# PACKAGE HASH:
# ============
#
#   name              => $(PKG_NAME)       from Makefile
#   version           => $(PKG_VERSION)    from Makefile
#   group             => $(PKG_GROUP)      from Makefile {app,base,dev,libs,net,...}
#
#   arch              => $toolchain        from comandline args
#   hardware          => $hardware         from comandline args
#   flavour           => $flavour          from comandline args for ROOT pkg, from REQUIRES for dependencies
#   tarball           => "$name-$version-$arch-$distro_name-$distro_version.$tarball_suffix"
#
#   distro_name       => $(DISTRO_NAME)    from build-system/constants.mk
#   distro_version    => $(DISTRO_VERSION) from build-system/constants.mk
#   url               => $(BUG_URL)        from build-system/constants.mk
#   license           =>                   from Makefile
#   short_description =>                   from Makefile
#   description       =>    first line     from .DESCRIPTION
#   uncompressed_size =>                   from .PKGINFO
#   total_files       =>                   from .PKGINFO
#
#   dir               => path to Makefile  from .$(HW)_requires
#   children          =>
#
sub fill_package_info
{
  my $base_dir = shift;
  my $makefile = shift;
  my $flavour  = shift;
  my ( $product_path, $tarball_file );
  my %pkg = ();

  $pkg{'dir'}     = $base_dir;
  $pkg{'install'} = pkg_rootfs_target( $makefile );

  $pkg{'name'} = pkg_name( $makefile );
  if( $pkg{'name'} eq "" )
  {
    # There is no package for this Makefile
    $pkg{'name'} = $pkg{'dir'};
    return %pkg;
  }

  $pkg{'version'}           = pkg_version( $makefile );
  $pkg{'arch'}              = $toolchain;
  $pkg{'hardware'}          = $hardware;
  $pkg{'flavour'}           = $flavour;
  $pkg{'group'}             = pkg_group( $makefile );
  $pkg{'distro_name'}       = $distro_name;
  $pkg{'distro_version'}    = $distro_version;
  $pkg{'url'}               = $url;
  $pkg{'license'}           = pkg_license( $makefile );
  $pkg{'short_description'} = pkg_short_description( $makefile );

  $pkg{'tarball'} = $pkg{'name'}    . "-" . 
                    $pkg{'version'} . "-" . 
                    $pkg{'arch'}    . "-" . 
                    $distro_name    . "-" . 
                    $distro_version . "." . 
                    $tarball_suffix;

  $pkg{'description'}       = $pkg{'name'} . " " . $pkg{'version'} . " (" . $pkg{'short_description'} . ")";
  $pkg{'uncompressed_size'} = '';
  $pkg{'total_files'}       = '';

  $product_path = $topdir . "/dist/products/" . $toolchain . "/" . $hardware;
  if( $flavour eq "" )
  {
    $tarball_file = $product_path . "/" . $pkg{'group'} . "/" . $pkg{'tarball'};
  }
  else
  {
    $tarball_file = $product_path . "/" . $pkg{'group'} . "/" . $pkg{'flavour'} . "/" . $pkg{'tarball'};
  }

  if( -e $tarball_file )
  {
    my $cleanup = 1;
    my $fname   = "$target_build_dir/.$hardware.pkginfo.XXXXXXXX";
    my $tempname;

    (undef, $tempname) = File::Temp::tempfile( $fname, OPEN => 0, UNLINK => $cleanup );

    _kxLab::system( "xzcat $tarball_file | tar -xvf - \".PKGINFO\" -O  1> $tempname  2> /dev/null" );

    $pkg{'uncompressed_size'} = get_pkg_info( $tempname, "uncompressed_size" );
    $pkg{'total_files'}       = get_pkg_info( $tempname, "total_files" );

    unlink $tempname;
  }

  return %pkg;
}


sub print_package_head
{
  my ( $level, $pkg )  = @_;
  my $indent = "";

  $level *= 2;
  while( $level )
  {
    $indent .= " ";
    $level--;
  }
  print JS_TREE_FILE $indent . "{\n";

  if( $pkg->{'name'} eq $pkg->{'dir'} )
  {
    if( $root_node == 1 )
    {
      print JS_TREE_FILE $indent . " \"distro\": ["
                                 . " \"" . $distro_name . "\","
                                 . " \"" . $distro_version . "\","
                                 . " \"" . $url . "\""
                                 . " ],\n";
    }
    print JS_TREE_FILE $indent . " \"name\": \"" . $pkg->{'name'} . "\"";
  }
  else
  {
    if( $root_node == 1 )
    {
      print JS_TREE_FILE $indent . " \"distro\": ["
                                 . " \"" . $distro_name . "\","
                                 . " \"" . $distro_version . "\","
                                 . " \"" . $url . "\""
                                 . " ],\n";
    }
    if( !defined $pkg->{'group'} || $pkg->{'group'} eq "" )
    {
      print JS_TREE_FILE $indent . " \"name\": \""
                                 . $pkg->{'name'} . "-"
                                 . $pkg->{'version'} . "\"";
    }
    else
    {
      print JS_TREE_FILE $indent . " \"name\": \""
                                 . $pkg->{'group'} . ":"
                                 . $pkg->{'name'} . "-"
                                 . $pkg->{'version'} . "\"";
    }
  }
}

sub print_package_start_children
{
  my $level = shift;
  my $indent = "";

  $level *= 2;
  while( $level )
  {
    $indent .= " ";
    $level--;
  }
  print JS_TREE_FILE $indent . " \"children\": [\n";
}

sub print_package_finish_children
{
  my $level  = shift;
  my $indent = "";

  $level *= 2;
  while( $level ) { $indent .= " "; $level--; }
  print JS_TREE_FILE $indent . " ]\n";
}

sub print_package_tail
{
  my $level  = shift;
  my $indent = "";

  $level *= 2;
  while( $level ) { $indent .= " "; $level--; }
  print JS_TREE_FILE $indent . "}";
}

sub print_comma
{
  my $comma = shift;

  if( $comma > 0 ) { print JS_TREE_FILE ",\n"; }
  else             { print JS_TREE_FILE  "\n"; }
}


#
# Parse the command line options
#

# Get the rest arguments of the command line
$topdir    = shift;
$toolchain = shift;
$hardware  = shift;
$flavour   = shift;

my $makefile = "Makefile";

if( ! defined $topdir    or $topdir eq "" )    { usage; }
if( ! defined $toolchain or $toolchain eq "" ) { usage; }
if( ! defined $hardware  or $hardware eq "" )  { usage; }
if( ! defined $flavour   or $flavour eq "" )   { $flavour = ""; }

_kxLab::error( "$0: $topdir is not a directory" ) if( ! -d $topdir );
_kxLab::error( "$0: Makefile missing: $makefile" ) if ( ! -f $makefile );

$build_system = $topdir . "/build-system";

$system_version = system_version( $build_system );
$distro_name    = distro_name( $build_system );
$distro_version = distro_version( $build_system );
$url            = bug_url( $build_system );


if( $flavour eq "" )
{
  $target_build_dir  = "." . $toolchain . "/" . $hardware;
}
else
{
  $target_build_dir  = "." . $toolchain . "/" . $hardware . "/" . $flavour;
}

$requires_file  = $target_build_dir . "/.requires";

if( $flavour eq "" )
{
  $html_tree_file = $target_build_dir . "/" . $hardware . ".tree.html";
  $js_tree_file   = $target_build_dir . "/" . $hardware . ".tree.json";
  $js_pkgs_file   = $target_build_dir . "/" . $hardware . ".pkgs.json";
}
else
{
  $html_tree_file = $target_build_dir . "/" . $hardware . "-" . $flavour . ".tree.html";
  $js_tree_file   = $target_build_dir . "/" . $hardware . "-" . $flavour . ".tree.json";
  $js_pkgs_file   = $target_build_dir . "/" . $hardware . "-" . $flavour . ".pkgs.json";
}

my $jsmin = $ENV{JSMIN};
if( $jsmin ne "" )
{
  $js_min_tree_file = $js_tree_file;
  $js_min_tree_file =~ s/\.json$/\.min\.json/;

  $js_min_pkgs_file = $js_pkgs_file;
  $js_min_pkgs_file =~ s/\.json$/\.min\.json/;
}

# open the intput file
open(REQUIRES_FILE, "< $requires_file") or
  _kxLab::error( "$0: Could not open $requires_file file: $!" );
# open the output files
open(JS_TREE_FILE, "> $js_tree_file") or
  _kxLab::error( "$0: Could not open $js_tree_file file: $!" );
open(JS_PKGS_FILE, "> $js_pkgs_file") or
  _kxLab::error( "$0: Could not open $js_pkgs_file file: $!" );


my $depth    = 2;
my $level    = 0;
my $root     = get_root();
my @treedirs = get_treedirs();
my %deps     = get_deps();

sub print_tree
{
  my ($level, $last, $pkg) = @_;

  if( $depth < $level ) { $depth = $level; }

  print_package_head( $level, \%{$pkg} );
  $root_node = 0;

  if( $pkg->{'children'} )
  {
    print_comma( 1 );
    print_package_start_children( $level );

    my @a = @{$pkg->{'children'}};
    my $n = $#a;

    foreach my $p ( @{$pkg->{'children'}} )
    {
      print_tree( $level + 1, $n--, \%{$p} );
    }

    print_package_finish_children( $level );
  }
  else
  {
    print_comma( 0 );
  }
  print_package_tail( $level );
  print_comma( $last );
}


#
# This is the root package
#
%tree = fill_package_info( $root, $makefile, $flavour );


my %sequence;
my $order = 0;

#################################################################
# if( there is any dependencies )
#
if( %deps )
{
  my @dep_keys = keys %deps;

  my $count = scalar( keys %deps );

  foreach my $dir ( @treedirs )
  {
    if( ! grep { $_ eq $dir } @dep_keys )
    {
      my $key = $dir;
      $sequence{$key} = ++$order;
      @treedirs = grep { $_ ne $key } @treedirs;

      # Split dir^flavour:
      my ($d, $f);
      $d = `echo $dir | cut -f 1 -d '^'`;
      $d =~ s/^\s+|\s+$//;
      if( $dir =~ m/\^/ )
      {
        $f = `echo $dir | cut -f 2 -d '^'`;
        $f =~ s/^\s+|\s+$//;
      }
      else
      {
        $f = "";
      }

      # Insert into sub_trees:
      my %pkg = fill_package_info( $d, $topdir . "/" . $d . "/Makefile", $f );
      $sub_trees{$dir} = \%pkg;

      delete $deps{$dir};
    }
  }


  for( my $i = 0; $i < $count; ++$i )
  {
    my @installed = keys %sequence;

    foreach my $key (sort keys %deps)
    {
      my $ok = 1;
      my @dirs = split( ' ', $deps{$key} );

      if( $key ne "all" )
      {
        foreach my $dir ( @dirs )
        {
          if( ! grep { $_ eq $dir } @installed )
          {
            $ok = 0;
          }
        }

        if( $ok == 1 )
        {
          $sequence{$key} = ++$order;

          # Split dir^flavour:
          my ($d, $f);
          $d = `echo $key | cut -f 1 -d '^'`;
          $d =~ s/^\s+|\s+$//;
          if( $key =~ m/\^/ )
          {
            $f = `echo $key | cut -f 2 -d '^'`;
            $f =~ s/^\s+|\s+$//;
          }
          else
          {
            $f = "";
          }

          # create package node:
          my %pkg = fill_package_info( $d, $topdir . "/" . $d . "/Makefile", $f );
          # add children:
          foreach my $dir ( @dirs )
          {
            my $child = $sub_trees{$dir};
            push( @{$pkg{'children'}}, $child );
          }

          # insert new sub tree into $sub_tree:
          $sub_trees{$key} = \%pkg;

          delete $deps{$key};
        }
      }
    }
  }

  #
  # The root node children
  #
  my @dirs = split( ' ', $deps{'all'} );
  foreach my $dir ( @dirs )
  {
    my $child = $sub_trees{$dir};
    push( @{$tree{'children'}}, $child );
  }

}
else
{
  my %pkg;

  $pkg{'dir'}  = "void";
  $pkg{'name'} = "void";

  push( @{$tree{'children'}}, \%pkg );
}
#
# End if( there is any dependencies )
#################################################################


print_tree( $level, 0, \%tree );

# close tree JSON file:
close JS_TREE_FILE;


#################################################################
# Calculate SVG size and make HTML from template:
#
my $copyright_url = $url;
my $html_template = $build_system . "/html/requires_tree_html.template";
my $w = $depth;
my $h = get_ntreedirs();
my ($width, $height);

$h = $h * 4 / 5;

$width  = ($w + 4) * 160;
$height = ($h + 4) * 24;

$root =~ s/\//\\\//g;
$root =~ s/\-/\\\-/g;
$root =~ s/\+/\\\+/g;
$root =~ s/\./\\\./g;

$copyright_url =~ s/\//\\\//g;
$copyright_url =~ s/\-/\\\-/g;
$copyright_url =~ s/\+/\\\+/g;
$copyright_url =~ s/\./\\\./g;



my $js_tree_file_name = basename( $js_tree_file );
my $js_pkgs_file_name = basename( $js_pkgs_file );

if( $jsmin ne "" )
{
  # minimize JSON tree file:
  _kxLab::system( "$jsmin -o $js_min_tree_file $js_tree_file" );
  $js_tree_file_name = basename( $js_min_tree_file );
  $js_pkgs_file_name = basename( $js_min_pkgs_file );
  # minimize JSON pkgs file later.
}

my $copyright = "Radix cross Linux";

my ( $second, $minute, $hour, $day, $month, $year );

#my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();

$second = localtime->sec();
$minute = localtime->min();
$hour   = localtime->hour();
$day    = localtime->mday();
$month  = localtime->mon() + 1;
$year   = localtime->year() + 1900;

my $call_string = sprintf( "cat %s | "                          .
                           "sed 's/\@HARDWARE\@/%s/g' | "       .
                           "sed 's/\@ROOT\@/%s/g' | "           .
                           "sed 's/\@TARBALL_SUFFIX\@/%s/g' | " .
                           "sed 's/\@BUG_URL\@/%s/g' | "        .
                           "sed 's/\@COPYING\@/%s/g' | "        .
                           "sed 's/\@YEAR\@/%s/g' | "           .
                           "sed 's/\@MONTH\@/%s/g' | "          .
                           "sed 's/\@DAY\@/%s/g' | "            .
                           "sed 's/\@HOUR\@/%s/g' | "           .
                           "sed 's/\@MINUTE\@/%s/g' | "         .
                           "sed 's/\@SECOND\@/%s/g' | "         .
                           "sed 's/\@SVG_WIDTH\@/%d/g' | "      .
                           "sed 's/\@SVG_HEIGHT\@/%d/g' | "     .
                           "sed 's/\@JSON_PKGS_FILE\@/%s/g' | " .
                           "sed 's/\@JSON_TREE_FILE\@/%s/g' > " . $html_tree_file,
                           $html_template,
                           $hardware,
                           $root,
                           $tarball_suffix,
                           $copyright_url,
                           $copyright,
                           $year, $month,  $day,
                           $hour, $minute, $second,
                           $width, $height,
                           $js_pkgs_file_name,
                           $js_tree_file_name );
_kxLab::system( $call_string );

#
# End of creating HTML file
#################################################################


# close input file:
close REQUIRES_FILE;


#################################################################
# Write packages JSON file and count the number of packages:
#
sub compare_order
{
  $sequence{$a} <=> $sequence{$b};
}

print JS_PKGS_FILE "[";

my $packages_done = 0;

sub print_result
{
  my $out_string = sprintf( "####### Required Packages Tree (done: %4d packages)\n", $packages_done );

  print $out_string;
}

print "#######\n";

foreach my $dir (sort compare_order( (keys %sequence) ))
{
  my $package;

  if( $dir ne "all" )
  {
    $package = $sub_trees{$dir};

    if( $package->{'install'} ne "no" )
    {

      if( $packages_done == 0 )
      {
        print JS_PKGS_FILE "{\n";
      }
      else
      {
        print JS_PKGS_FILE ",\n";
        print JS_PKGS_FILE " {\n";
      }

      if( !defined $package->{'group'} || $package->{'group'} eq "" )
      {
        print JS_PKGS_FILE "  \"id\": \""
                           . $package->{'name'} . "-"
                           . $package->{'version'} . "\",\n";
      }
      else
      {
        print JS_PKGS_FILE "  \"id\": \""
                           . $package->{'group'} . ":"
                           . $package->{'name'} . "-"
                           . $package->{'version'} . "\",\n";
      }
      print JS_PKGS_FILE "  \"name\": \""     . $package->{'name'}     . "\",\n";
      print JS_PKGS_FILE "  \"version\": \""  . $package->{'version'}  . "\",\n";
      print JS_PKGS_FILE "  \"group\": \""    . $package->{'group'}    . "\",\n";
      print JS_PKGS_FILE "  \"arch\": \""     . $package->{'arch'}     . "\",\n";
      print JS_PKGS_FILE "  \"hardware\": \"" . $package->{'hardware'} . "\",\n";
      if( defined $package->{'flavour'} && $package->{'flavour'} ne "" )
      {
        print JS_PKGS_FILE "  \"flavour\": \"" . $package->{'flavour'}  . "\",\n";
      }
      print JS_PKGS_FILE "  \"license\": \""           . $package->{'license'}           . "\",\n";
      print JS_PKGS_FILE "  \"description\": \""       . $package->{'description'}       . "\",\n";
      print JS_PKGS_FILE "  \"uncompressed_size\": \"" . $package->{'uncompressed_size'} . "\",\n";
      print JS_PKGS_FILE "  \"total_files\": \""       . $package->{'total_files'}       . "\"\n";
      print JS_PKGS_FILE " }";

      ++$packages_done;
    }
  }
}

#
# Print the root node of tree:
#
my $tree = \%tree;

if( $tree->{'install'} ne "no" )
{

  if( $packages_done == 0 )
  {
    print JS_PKGS_FILE "{\n";
  }
  else
  {
    print JS_PKGS_FILE ",\n";
    print JS_PKGS_FILE " {\n";
  }

  if( !defined $tree->{'group'} || $tree->{'group'} eq "" )
  {
    print JS_PKGS_FILE "  \"id\": \""
                       . $tree->{'name'} . "-"
                       . $tree->{'version'} . "\",\n";
  }
  else
  {
    print JS_PKGS_FILE "  \"id\": \""
                       . $tree->{'group'} . ":"
                       . $tree->{'name'} . "-"
                       . $tree->{'version'} . "\",\n";
  }
  print JS_PKGS_FILE "  \"name\": \""     . $tree->{'name'}     . "\",\n";
  print JS_PKGS_FILE "  \"version\": \""  . $tree->{'version'}  . "\",\n";
  print JS_PKGS_FILE "  \"group\": \""    . $tree->{'group'}    . "\",\n";
  print JS_PKGS_FILE "  \"arch\": \""     . $tree->{'arch'}     . "\",\n";
  print JS_PKGS_FILE "  \"hardware\": \"" . $tree->{'hardware'} . "\",\n";
  if( defined $tree->{'flavour'} && $tree->{'flavour'} ne "" )
  {
    print JS_PKGS_FILE " \"flavour\": \"" . $tree->{'flavour'}  . "\",\n";
  }
  print JS_PKGS_FILE " \"license\": \""           . $tree->{'license'}           . "\",\n";
  print JS_PKGS_FILE " \"description\": \""       . $tree->{'description'}       . "\",\n";
  print JS_PKGS_FILE " \"uncompressed_size\": \"" . $tree->{'uncompressed_size'} . "\",\n";
  print JS_PKGS_FILE " \"total_files\": \""       . $tree->{'total_files'}       . "\"\n";
  print JS_PKGS_FILE " }";

  ++$packages_done;
}

print JS_PKGS_FILE "]\n";

print_result();
print "#######\n";
#
# End of packages JSON file write.
#################################################################

# close pkgs JSON file:
close JS_PKGS_FILE;

if( $jsmin ne "" )
{
  # minimize JSON tree file:
  _kxLab::system( "$jsmin -o $js_min_pkgs_file $js_pkgs_file" );
}