#!/usr/bin/env perl -w
#
#                             stb225 Project
#                   ---------------------------------
#
# Copyright (C) 2008 NXP B.V.,
# All Rights Reserved.
#
# Filename: prjconfig2cmds.pl
#
# - Converts .config variables into shell commands to set up the environment.
# - Generates .prjconfig file. This contains the .config contents,
# and additional variables set by invocations of addConfigFile*Var() functions.
# - Appends to prjconfig.h variables set by invocations of addConfigFile*Var().
#
# This file script is the generic master script, it needs a project specific
# Perl file as an argument 1.
# That file must define a Perl subroutine called 'processConfig'.
#
# Usage: perl prjconfig2cmds.pl PRJCONFIG2CMDS.DEF.pl DOT_CONFIG_FILE
#
# Environment variable PRJCONFIGOPTS may contain:
# - -d (for debugging this script)
#
# Rev Date       Author      Comments
#-------------------------------------------------------------------------------
#   1 20060517   batelaan    Initial
#   2 20060609   batelaan    Ignore blank lines in .config file.
#   3 20061201   batelaan    Add some util funcs. Generate .prjconfig file.
#                            Append to prjconfig.h file.
#   4 20061206   batelaan    Correct $maxInt value.
#   5 20070117   batelaan    Correct error handling after 'do' statement.
#   6 20070914   batelaan    Ensure prjconfig.h exists before we append to it.
#   7 20070914   batelaan    Add stringExpand function.
#   8 20080121   hutch       Need Perl v5.8 or higher for BigInt pkg.
#   9 20080305   batelaan    Optimise .config file parser.
#8.1.1 20080326  batelaan    Include guards for prjconfig.h
#  10 20080305   batelaan    Merge
#  11 20080722   batelaan    Add generic variable code from prjconfig2cmds.def.pl;
#                            Generate .config.d directory to track dependencies
#                            on individual config variables.
#  12 20080828   batelaan    Correct BUILDROOT_PATH_FMT (stringExpand).
#                            Rework some syntax to be more robust.
#  13 20080912   batelaan    Add Carp module and $configFileDir.

# 5.10 perl gives "v-string in use/require non-portable" msg if we don't use
# "5.008_000" format for specifying the perl version.
# Need 5.8 for some features of Math::BigInt;
use 5.008_000;

use Carp;
use Getopt::Std;
use vars qw($opt_h $hash $configFileDir);
use Math::BigInt;
use File::Copy;
use File::Basename;

$pgm = $0;
$errors = 0;
$opt_d = 0;
if (exists $ENV{PRJCONFIGOPTS}) {
  $opt_d = $ENV{PRJCONFIGOPTS} =~ /-d/;
}

our $maxInt = new Math::BigInt(hex '0x7FFFFFFF');

sub usage {
    print STDERR <<EOF;
@_
$pgm [<options>] prjconfig2cmds.def.pl .config >...
To be used by prjconfig utility
Options:
  -h		Help.
  -d            Debug.
EOF
  exit($opt_h ? 0 : 1);
}

usage() if (!getopts("dh") or $opt_h);
usage("Incorrect number of arguments.") unless @ARGV == 2;
$defFile = shift;
$configFile = shift;
$configFileDir = dirname($configFile);
@additionalVars = ();
%knownValueTypes =
(
 'Math::BigInt' => 1,
 '' => 1,
);
%oldConfig = (); # from last run (.config.d/config.cached)
%newConfig = (); # the new config we want to set.

################################################################################
sub main {
  do $defFile;
  fatal("$defFile: $@\n") if $@;

  if ($configFile !~ /\.config$/) {
    fatal("Config filename should end with \".config\"\n");
  }
  readConfigFile($configFile, \%newConfig);
  exportConfigAsVariables(\%newConfig);
  processConfigCommon();
  processConfig(); # defined in $defFile Perl file
  setupConfigDependenciesDir($configFile);
  updateConfigFiles($configFile);
}

################################################################################
# Handle/setup:
# - CONFIG_NAME
# - PRJROOT
# - BUILDROOT
sub processConfigCommon {
  # Check CONFIG_NAME
  if ($CONFIG_NAME && $CONFIG_NAME ne '')
  {
    my $config_name_format = '[a-zA-Z][a-zA-Z0-9_]*';
    fatal("CONFIG_NAME variable should start with a letter, followed by letters, digits, and underscores: \"$CONFIG_NAME\"\n") unless
    $CONFIG_NAME =~ /^$config_name_format$/;
  }
  else
  {
     fatal("Each configuration must be given a name to allow for different build outputs. PLEASE SET CONFIG_NAME\n");
  }

  # Setup Environment for building
  $PRJROOT = $ENV{'PRJROOT'};

  # Set BUILDROOT variable.
  if (exists $ENV{'BUILDROOT_PATH_FMT'}) {
    $BUILDROOT = stringExpand($ENV{'BUILDROOT_PATH_FMT'});
  } else {
    $BUILDROOT = "$PRJROOT/build_$CONFIG_NAME";
  }
  setEnv("BUILDROOT", $BUILDROOT);
}

################################################################################
# Create $BUILDROOT/.config.d directory
# with a .h file for each config variable in %oldConfig
# and %newConfig.
# Only create/touch each file if its config variable has changed.
sub setupConfigDependenciesDir {
  my ($configFile) = @_;
  my $depsDir = "$BUILDROOT/.config.d";
  my $oldConfigFile = "$depsDir/config.cache";
  my $firstTime = 1;
  if (-f $oldConfigFile) {
    readConfigFile($oldConfigFile, \%oldConfig);
    $firstTime = 0;
  } else {
    if (! -d $BUILDROOT) {
      mkdir($BUILDROOT, 0755) || fatal("$!: mkdir $BUILDROOT\n");
    }
    mkdir($depsDir, 0755) || fatal("$!: mkdir $depsDir\n");
    if (! -f "$depsDir/AAREADME.txt") {
      open(my $F, ">$depsDir/AAREADME.txt");
      print $F <<EOT;
This directory is maintained by prjconfig. Do not touch or remove!
Contents is used to track dependencies on individual configuration variables.
EOT
      close $F;
    }
  }
  my %allVars;
  map {$allVars{$_}++} (keys(%oldConfig), keys(%newConfig));
  foreach my $v (keys %allVars) {
    # create/touch a file for this variable if its the first time,
    # or if the variable appears only in one of the old/new configs,
    # or the value has changed:
    if ($firstTime || $allVars{$v} == 1 || $oldConfig{$v} ne $newConfig{$v}) {
      print STDERR
        "Changed: $v (old: ",
        (exists $oldConfig{$v} ? $oldConfig{$v} : '<notset>'),
        " new: ",
        (exists $newConfig{$v} ? $newConfig{$v} : '<notset>'),
        ")\n" if $opt_d;
      open(my $F, '>', "$depsDir/$v.h") || fatal("$!: $depsDir/$v.h\n");
      close $F;
    }
  }
  copy($configFile, $oldConfigFile) || fatal("$!: copy $configFile to $oldConfigFile\n");
}

################################################################################
sub readConfigFile {
  my $configFile = shift;
  local $main::hash = shift;
  fatal("$configFile: $!\n") unless open(my $F, $configFile);
  local $_;
  while (<$F>) {
    next if /^#/;
    next unless /\S/; # ignore blank lines
    print STDERR $_ if $opt_d;
    s/=n$/=0/;
    s/=y$/=1/;
    s/^(.*?)\s*=/\$main::hash->{$1} = /;
    $_ .= ';';
    eval $_;
    fatal("$configFile:$.: $_  $@\n") if $@;
  }
  close $F;
}

################################################################################
# Make all keys in the hashtable global variables with value
# specified by the hash entry.
sub exportConfigAsVariables {
  my ($hash) = @_;
  no strict 'refs';
  foreach my $k (keys(%$hash)) {
    my $v = $hash->{$k};
    $$k = $v;
  }
}

################################################################################
# Updates prjconfig.h file. Writes new prjconfig.mk file.
sub updateConfigFiles {
  my ($configFile) = @_;

  my $prjconfigFile = $configFile;
  $prjconfigFile =~ s/\.config$/.prjconfig/;

  # copy $configFile to $prjconfigFile:
  unless (open(CONFIG, $configFile)) {
    fatal("$!: $configFile\n");
  }
  unless (open(PRJCONFIG, ">$prjconfigFile")) {
    fatal("$!: $prjconfigFile\n");
  }
  while (<CONFIG>) {
    print PRJCONFIG;
  }
  close CONFIG;

  my $prjconfigHeaderFile = $configFile;
  $prjconfigHeaderFile =~ s/\.config$/prjconfig.h/;
  fatal("$!: $prjconfigHeaderFile\n") unless -f $prjconfigHeaderFile;
  unless (open(PRJCONFIGHEADER, ">>$prjconfigHeaderFile")) {
    fatal("$!: $prjconfigHeaderFile\n");
  }

  print PRJCONFIG "\n#\n# Calculated variables follow.\n#\n\n";
  print PRJCONFIGHEADER <<EOT;

/* Calculated variables follow. */
#ifndef PRJCONFIG_H_CALCULATED
#define PRJCONFIG_H_CALCULATED

EOT

  printConfigFileVars();

  print PRJCONFIGHEADER <<EOT;

#endif /* PRJCONFIG_H_CALCULATED */
EOT
  close PRJCONFIG;
  close PRJCONFIGHEADER;
}

################################################################################
# Print each entry from @additionalVars to updated/generated config files.
# Need to handle different types of variables (string/decimal/hex).
# Variable options are:
# - numeric/string
# - decimal/hexadecimal
# - int/long (ignored)
sub printConfigFileVars {
  foreach my $a (@additionalVars) {
    my ($type, $var, $value) = @$a;
    my $printValue = $value;
    print STDERR "Adding config file '$type' var $var = \"$value\" (value type = " . ref($value) . ")\n" if $opt_d;

    unless (exists $knownValueTypes{ref($value)}) {
      fatal("Unknown variable '$var' (type: " . ref($value) . "): $value\n");
    }

    if ($type eq 'decimal') {
      if (ref($value) eq 'Math::BigInt') {
	$printValue = $value->bstr();
      } else {
	$printValue = sprintf '%d', $value;
      }
    }

    if ($type eq 'hex') {
      if (ref($value) eq 'Math::BigInt') {
	$printValue = $value->as_hex();
      } else {
	$printValue = sprintf '0x%x', $value;
      }
    }

    my $makefileVarValue = $printValue;
    print PRJCONFIG       "CONFIG_$var=$makefileVarValue\n";
    my $cVarValue = $value;
    if ($type eq 'string') {
      $cVarValue =~ s/\\/\\\\/g;
      $cVarValue =~ s/\n/\\\n/g;
      $cVarValue =~ s/"/\\"/g;
    } else {
      $cVarValue = $printValue;
    }
    print PRJCONFIGHEADER "#define CONFIG_$var $cVarValue\n";
  }
}

################################################################################
# Adds a string variable with the specified name and value to the generated
# configuration files. (The variable name is prefixed with 'CONFIG_').
# Value will be quoted by this function.
#
# A perl variable with name $var is also set to the specified value
# unless it is already defined.
#
# Usage: addConfigFileStringVar("var", $stringValue);
sub addConfigFileStringVar ($$) {
  addConfigFileVar('string', @_);
}

################################################################################
# Adds an numeric decimal variable with the specified name and value to the generated
# configuration files. (The variable name is prefixed with 'CONFIG_').
#
# A perl variable with name $var is also set to the specified value
# unless it is already defined.
#
# Usage: addConfigFileDecimalVar("var", $numericValue);
sub addConfigFileDecimalVar ($$) {
  addConfigFileVar('decimal', @_);
}

################################################################################
# Adds a numeric hex variable with the specified name and value to the generated
# configuration files. (The variable name is prefixed with 'CONFIG_').
#
# A perl variable with name $var is also set to the specified value
# unless it is already defined.
#
# Usage: addConfigFileHexVar("var", $numericValue);
sub addConfigFileHexVar ($$) {
  addConfigFileVar('hex', @_);
}

################################################################################
# Utility
sub addConfigFileVar {
  my ($type, $var, $value) = @_;
  no strict 'refs';
  unless (defined $$var) {
    $$var = $value;
  }
  push @additionalVars, [@_];
}

################################################################################
# Usage: setEnv("var", "value");
# Value is printed inside double quotes, so ensure you escape as needed.
sub setEnv {
  my ($var, $value) = @_;
  print STDERR "Setting $var to \"$value\"\n" if $opt_d;
  print "$var=\"$value\"\n";
  print "export $var\n";
  $ENV{$var} = $value;
}

################################################################################
# Usage: setEnvConditionally("var", "value", $condition);
# Call setEnv if $condition is true, else call unsetEnv.
sub setEnvConditionally {
  my ($var, $value, $condition) = @_;
  if ($condition) {
    setEnv($var, $value);
  } else {
    unsetEnv($var);
  }
}

################################################################################
# Usage: unsetEnv("var");
# Unsets environment variable.
sub unsetEnv {
  my ($var) = @_;
  print STDERR "Unsetting $var\n" if $opt_d;
  print "unset $var\n";
}

################################################################################
sub msg {
  print STDERR "@_";
}

################################################################################
sub info {
  msg "(info)", @_;
}

################################################################################
sub warning {
  msg "**WARNING:", @_;
}

################################################################################
sub error {
  msg "**ERROR:", @_;
  $main::errors++;
}

################################################################################
sub fatal {
  msg "**FATAL:", @_;
  exit 1;
}

################################################################################
# Returns a Math::BigInt instance, or dies.
# Usage: $bigInt = parseBigInt($valueString, $whatWeAreParsing);
# Error message is: "Invalid big integer $whatWeAreParsing: ($valueString)".
sub parseBigInt {
  my ($valueString, $what) = @_;
  my $s = $valueString;
  $s =~ s/\s+//g;
  my $factor = 1;
  if ($s =~ s/mb?$//i) {
    $factor = 1024 * 1024;
  } elsif ($s =~ s/kb?$//i) {
    $factor = 1024;
  }
  my $bigInt = new Math::BigInt $s;
  die "Invalid big integer $what: ($valueString)\nFormats: 123 -123 0x123 123K 123M\n" if $bigInt eq 'NaN';
  $bigInt->bmul(Math::BigInt->new($factor));
  $bigInt;
}

################################################################################
# Returns a integer (< 0x80000000), or dies.
# Usage: $int = getConfigInt($var);
# E.G.: $int = getConfigInt('MAIN_MIPS_MEM_SIZE');
# Error message is: "Integer $var is >= 0x80000000: ($$var)" or
# the message generated by getConfigBigInt.
sub getConfigInt {
  my ($var) = @_;
  no strict 'refs';
  my $i = getConfigBigInt(@_);
  if ($i > $maxInt) {
    die "Integer $var is > 0x7FFFFFFF: ($$var)\n";
  }
  $i->numify();
}

################################################################################
# Returns a Math::BigInt instance, or dies.
# Usage: $bigInt = getConfigBigInt($var);
# E.G.: $bigInt = getConfigBigInt('CONFIG_MAIN_MIPS_MEM_SIZE');
# Error message is: "Invalid big integer $var: ($$var)".
sub getConfigBigInt {
  my ($var) = @_;
  no strict 'refs';
  my $configVar = "CONFIG_$var";
  parseBigInt($$configVar, $var);
}

# Return string with variables (environment or Perl) expanded.
# Usage: $var = stringExpand('Say hello to $NAME');
sub stringExpand ($) {
  my ($s) = @_;
  my $result = $s;
  no strict 'refs';
  $result =~ s{\${?(\w+)}?} {
    exists $ENV{$1} ? $ENV{$1} :
      (defined ${$1} ? ${$1} :
       fatal("stringExpand(\"$s\"): $1 is not an environment variable nor a Perl variable\n"))
  }ge;
  print STDERR "stringExpand(\"$s\") => \"$result\"\n" if $opt_d;
  $result;
}

################################################################################
main();
exit($main::errors == 0 ? 0 : 1);
################################################################################
