#!/usr/local/bin/perl
##-------------------------------------------------------------------------
## 
## makemake.pl -- Utility to create Makefiles
## 
## Author          : Pierre Jaccard
## Created On      : 1999/11/04 23:26:39
## Last Modified By: Pierre Jaccard
## Last Modified On: 1999/11/17 11:50:05
## Update Count    : 143
## Directory       : /pcdata1/jaccard/codas3c/gfi/make/
## Version         : 0.0
## Status          : Unknown
## --------------------------------------------------------------------- ++
## DESCRIPTION: 
## 
##   See separate documentation
## 
## --------------------------------------------------------------------- ++
## REVISIONS: 
## --------------------------------------------------------------------- ++
## CHANGES: 
##-------------------------------------------------------------------------

use strict;
use Getopt::Std;
use IO::File;
use File::Basename;

#------------------------------------------------------------------
# File Variables:
#------------------------------------------------------------------

my (%OPTS, %data, %regs, @srcs, %MM, @MM);

@MM = ('src', 'obj', 'lib', 'bin', 'srcs', 'objs', 'libs', 
       'bins', 'target');
foreach(@MM){
  $MM{$_} = '';
}
       
#------------------------------------------------------------------
# Options:
#------------------------------------------------------------------
{
  my($opt_str, $tmp_str, $c);

  $opt_str = 'b:f:l:o:s:t:D:O:P:S:T:';

  if($] < 5.004){
    my($tmp_str);
    
    $tmp_str = $opt_str;
    while($tmp_str){
      $c = chop($tmp_str);
      next if($c !~ /\w/);
      eval( 'use vars qw($opt_' . $c . '); $opt_' . $c . ' = 0;' );
    }
    getopts($opt_str) || die("\n\n");
    $tmp_str = $opt_str;
    while($tmp_str){
      $c = chop($tmp_str);
      next if($c !~ /\w/);
      eval( '$OPTS{' . $c . '} = $opt_' . $c . ' if($opt_' . $c .');' );
    }
  }
  else{
    getopts($opt_str, \%OPTS) || die("\n\n");
  }
    
}

#--- Defaults:

$OPTS{'D'} = ''          unless $OPTS{'D'};
$OPTS{'O'} = '-'         unless $OPTS{'O'};
$OPTS{'P'} = 'MM_'       unless $OPTS{'P'};
$OPTS{'S'} = '_'         unless $OPTS{'S'};
$OPTS{'T'} = 'MMFile.mm' unless $OPTS{'T'};
$OPTS{'b'} = ''          unless $OPTS{'b'};
$OPTS{'f'} = 'MMFile'    unless $OPTS{'f'};
$OPTS{'l'} = '.a'        unless $OPTS{'l'};
$OPTS{'o'} = '.o'        unless $OPTS{'o'};
$OPTS{'s'} = '.c'        unless $OPTS{'s'};

#------------------------------------------------------------------
# Get the data for the given keyword, and build hash with
# regular expressions and replacement strings.
#------------------------------------------------------------------
{
  my($k);
  my($f, $t, $P, $S) = ($OPTS{'f'}, $OPTS{'t'}, $OPTS{'P'}, $OPTS{'S'});

  #--- Read definition file:
  read_data(\%OPTS, \%data)
    || die("\nNo definitions for $t in file $f\n") unless(%data);

  #---
  # Fills the data hash with defaults. This is required so that
  # mentionned variables in the template Makefile will expand to
  # the default string if not specified in the definition file.
  #---
  foreach $k (keys %MM){
    $data{$k} = $MM{$k} unless(exists $data{$k});
  }
  #--- Add suffixes:
  {
    my($t, $s);

    foreach $t ('src', 'srcs', 'lib', 'libs', 'bin', 'bins', 'obj', 'objs'){
      $s = $OPTS{(substr($t, 0, 1))};
      $data{$t} =~ s/\s*([^\s]+)\s*/$1$s /g; 
    }
  }
  
  #--- Build regexps and replacement strings:
  $P = quotemeta($P);
  $S = quotemeta($S);
  %regs = ();
  foreach $k (keys %data){
    $regs{$P . quotemeta($k) . $S} = $k;
  }
  
}

#------------------------------------------------------------------
# Select to create a makefile for a binary, a library or an
# object file.
#------------------------------------------------------------------

unless($data{'target'}){

  my($f, $t) = ($OPTS{'f'}, $OPTS{'t'});

 SWITCH:{

    if($data{'bin'}){
      $data{'target'} = 'bin';
      last SWITCH;
    }
    if($data{'lib'}){
      $data{'target'} = 'lib';
      last SWITCH;
    }
    if($data{'obj'}){
      $data{'target'} = 'obj';
      last SWITCH;
    }
    
    die("\nNo target specified for keyword $t in file $f\n");
  }

  #---
  # Some version of make do not like empty string labels. This may happen
  # if the template Makefile contains both lin and bin targets templates.
  # Hence, undefined targets have to be defined any way.
  #---
  foreach $t ('bin', 'lib', 'obj'){
    $data{$t} = "no_$t" unless($data{$t});
  }
}

#------------------------------------------------------------------
# Build the list of object files suspected to be made. This list
# is required to expand the dependencies. Only sources in the
# current directory will be expanded.
#------------------------------------------------------------------  
{
  my($s);

  @srcs = ();
  foreach $s (split(/\s+/, $data{'srcs'} . ' ' . $data{'src'})){
    push(@srcs, $s) unless($s =~ /\W\w+\.\w+$/);
  }
}

#------------------------------------------------------------------
# Write the new makefile:
#------------------------------------------------------------------
{
  my($ih, $oh, $k, $r, $e);
  my($skip);
  my($T, $O, $P, $S, $D) = ($OPTS{'T'}, $OPTS{'O'}, 
                            $OPTS{'P'}, $OPTS{'S'},
                            $OPTS{'D'});

  #---
  $r = quotemeta($P . 'OBJ_DEP' . $S);
  $e = quotemeta($P) . '.+' . quotemeta($S);

  #---
  $ih = new IO::File "< $T";
  die("\nOpening template $T for new Makefile\n") unless(defined $ih);
  $oh = new IO::File "> $O";
  die("\nOpening new Makefile $O\n") unless(defined $oh);
  while(<$ih>){
    chomp;

    #--- Expand definitions:
    foreach $k (keys %regs){
      #---
      # These are expanded only if a data is definied for them
      #---
      if(exists($data{$regs{$k}}) && $data{$regs{$k}}){
        s/$k/$data{$regs{$k}}/g;
        next;
      }
    }

    #--- 
    # Remaining definitions are replaced to a complete empty string if
    # within a MAKE definition, or just removed.
    #---
    $_ = $1 if(/^(\s*\w+\s*\=).*$e.*$/);
    s/$e//go unless(/$r/o);
    
    #--- Check for object dependencies:
    if($D && /$r/o){
      $_ = get_deps($D, \@srcs);
    }
    if(!$D && /$r/o){
      $_ = '';
    }

    print $oh "$_\n" unless($skip);

  }
  close($oh) || die("\nClosing new Makefile $O\n");
  close($ih) || die("\nClosing template $T for new Makefile $O\n");

}

exit(0);

#------------------------------------------------------------------
# Read common definitions. This function is also used to read
# target specific definitions.
#
# The current definiton is read until no line continuation 
# character ('\') is encountered at the end of current line.
# The value is then stored in the specified hash key.
#
# Comments are not allowed with definition declarations, and
# empty lines will be naturally ignored.
#------------------------------------------------------------------
sub read_common_def {

  my($v);

  my($fh, $def, $key, $val) = @_ ;

  #--- Loop while end of multi-line:
  while($val =~ /^\s*(.*?)\s*\\\s*$/){
    
    #--- Remove the line continuation:
    $val = $1;

    #--- Get next line:
    last unless($v = <$fh>);
    chomp($v);
    $v =~ s/^\s*(.*?)\s*$/$1/g;
    
    #--- Append line:
    $val = $val . ' ' . $v;
  }
 
  #--- Store the definition:  
  $def->{$key} = $val;

  return(! eof($fh));

}

#------------------------------------------------------------------
# Read definitions specific to target.
#
# No comment or empty line are allowed. Each target specific 
# definition must appear immediately after the previous one, 
# and must be indented by at least one space character.
#
# Definiton synthax is:
#
#   <keyword> = <value>
#
# see read_common_def() for more information. The group of
# target specific definitions is terminated by a line that
# does not correspond to the synthax above.
#------------------------------------------------------------------
sub read_target_defs {

  my($l);
  my($fh, $def) = @_ ;

  #--- Loop until no more definitions:
  while(($l = <$fh>) && ($l =~ /^\s+([\w_]+)\s*\=\s*(.*?)\s*$/)){
    chomp($l);
    last unless read_common_def($fh, $def, $1, $2);
  }

  return(1);

}

#------------------------------------------------------------------ 
# Read the definition file for creating the specific Makefile.
#
# Input synthax is the following:
#
#    - empty lines (containing only spaces) are ignored
#
#    - comment lines begin with the '#' character, eventually
#      preceeded by spaces
#
#    - common definitions have the synthax:
#
#        <keyword> = <value>
#
#      see read_common_def() for more information. They must not
#      be indented.
#
#    - definitions for a specific target have the following synthax:
#
#         <target>:
#            ... followed by the definitions ...
#
#      see read_target_defs() for more information. The <target>
#      keyword must not be indented.
#
# Common definitions are parsed until target specific definitions
# are encountered. These are read and reading of the file is
# immediately stopped. Everything appearing below target specific
# definitions is not parsed.
#------------------------------------------------------------------    
sub read_data {

  my($done, $f, $t, $fh);

  my($opts, $def) = @_ ;

  #--- Initializations:
  $done = 0;
  ($f, $t) = ($opts->{'f'}, $opts->{'t'});

  #--- Open definition file:
  $fh = new IO::File "< $f";
  die("\nOpening definition file $f\n") unless(defined $fh);

  #--- Loop through definition file:
  while(($_ = <$fh>) && !($done)){
    chomp;

    #--- Skip comment and empty lines:
    next if(/^\s*$/ || /^\s*\#/);

    #--- Check for common definitions:
    if(/^([\w_]+)\s*\=\s*(.*?)\s*$/){
      last unless read_common_def($fh, $def, $1, $2);
      next;
    }
    
    #--- Check for target definitions:
    if(/^([\w_]+)\s*\:\s*$/ && ($1 eq $t)){
      read_target_defs($fh, $def);
      $done = 1;
      last;
    }
  }
  close($fh) || die("\nClosing definition file $f\n");
  die("\nReading definitions for target $t from file $f\n")
    unless($done);
  
  return(1);

}

#------------------------------------------------------------------
# Create dependencies for object files.
#------------------------------------------------------------------
sub get_deps {

  my($s);
  my($str);
  my($cmd, $srcs) = @_ ;

  $str = '';

  foreach $s (@{$srcs}){
    open(PIPE, "$cmd $s |") 
      || die("\nOpening pipe from $cmd $s to get dependencies\n");
    while(<PIPE>){
      $str = $str . $_ ;
    }
    close(PIPE) 
      || die("\nClosing pipe from $cmd $s to get dependencies\n");
  }

  return($str);
}
