#!/usr/bin/perl -- # -*- perl -*-
#
# Occam For All project KROC (Kent Retargetable Occam Compiler)
# Makefile generator
#
# $Source: /u3/export/proj/ofa/kroc/RCS/kmakef.in,v $
#
# $Id: kmakef.in,v 1.10 1998/01/12 09:55:15 djb1 Exp $
#
# (C) Copyright 1996 Dave Beckett <D.J.Beckett@ukc.ac.uk>
# University of Kent at Canterbury, England
#

require 5.001;

use File::Basename;

use strict;

$::prog_name=basename($0);
$::verbose=0;
$::dryrun=0;

$::version= (qw$Id: kmakef.in,v 1.10 1998/01/12 09:55:15 djb1 Exp $)[2];


&main(@ARGV);

exit 0;


sub parse_filename ($) {
  my($file,$dir)=fileparse(shift);
  my $suffix='';
  if ($file =~ /^(.*)\.([^\.]+)$/) {
    ($file,$suffix)=($1,$2);
  }
  return ($dir,$file,$suffix);
}


sub get_rule ($$$$$) {
  my($file, $depends_on_ref, $dep_files_ref,
     $extra_inc_search_ref, $extra_lib_search_ref)=@_;
  my(@dep_files)=@$dep_files_ref; # copy

  my($dir,$prefix,$suffix)=parse_filename($file);
  $dir= '' if $dir eq './';

  my $incstr=(@$extra_inc_search_ref) ? " -I".join(' -I',@$extra_inc_search_ref) : '';
  my $libstr=(@$extra_lib_search_ref) ? " -L".join(' -I',@$extra_lib_search_ref) : '';

  if ($suffix eq '') {
    # Executables depend on source files and may need to link in
    # native libraries (libfoo.a)
    my $llibstr='';
    # Rewrite .../libfoo.a as -lfoo
    my(@libs)=grep(s%^.*lib(.+)\.a$%-l$1%, @dep_files);
    $llibstr=" @libs" if @libs;

    # or native object files (foo.o)
    my $objs='';
    my(@objs)=grep(/\.o$/, @dep_files);
    $objs=" @objs" if @objs;

    return "\$(KROC)$incstr$libstr $dir$prefix.occ$objs -o $prefix$llibstr";
  } elsif ($suffix eq 'o') {
    # Separately compiled file depends on source file 
    my $to='';
    $to=" -o $file" if $dir ne '';
    return "\$(KROC)$incstr$libstr -c $dir$prefix.occ$to";
  } elsif ($suffix eq 'occ' || $suffix eq 'inc') {
    # nothing to do
    return '';
  } elsif ($suffix eq 'a') {
    my $name=$prefix;
    $name =~ s%^lib%%;
    my $lbb_file="$dir$name.lbb";
    my $lib_file="$dir$name.lib";
    my(@objs)=grep(/\.o$/, @dep_files);
    return "\$(ILIBR) -f $lbb_file -o $lib_file\n" .
           "\t\$(DEL) $file\n" .
           "\t\$(AR) rc $file @objs\n" .
           "\t\$(RANLIB) $file";
  } else {
    warn "$::prog_name: I don't know how to build file suffix $suffix in '$file'\n";
  }
}


# Do a search for a file in a given path
# Maybe assume INMOS rules: search path begins with current directory
# of the file
# 
sub path_search ($$$) {
  my($file, $path_ref, $inmos_rules)=@_;

  # Absolute or relative path - don't search
  return $file if $file =~ m%/%;

  my(@path)=@$path_ref;
  if ($inmos_rules) {
    my $fdir=dirname $file;
    $fdir='.' if !length $fdir;
    unshift(@path, $fdir) unless grep($fdir eq $_, @path);
  }

  my $dir;
  foreach $dir (@path) {
    my $this_dir=$dir;
    $this_dir.="/" if $this_dir !~ m%/$%;
    $this_dir='' if $this_dir eq './';
    my $real_file=$this_dir.$file;

    return $real_file if -r $real_file;
  }
  return '';
}


sub get_occ_file_deps ($) {
  my $file=shift;
  
  my(@depends)=();
  open(IN, $file) or die "$::prog_name: Could not open $file - $!\n";
  while(<IN>) {
    chop;
    next if /^\s*--/;
    next if /^\s*#COMMENT/i;

    if (/#INCLUDE "([^"]+)"/i) {
      my $file=$1;
      push(@depends, $1);
    } elsif (/#USE "([^"]+)"/i) {
      my $file=$1;
      $file.='.tco' if $file !~ /\./;
      push(@depends, $file);
    }
  }
  close(IN);
  return @depends;
}


sub get_lib_file_deps ($) {
  my $file=shift;

  my $dir=dirname $file;
  $dir=($dir eq '.') ? '' : "$dir/";

  my(@depends)=();
  open(IN, $file) or die "$::prog_name: Could not open $file - $!\n";
  while(<IN>) {
    chop;
    s/^\s+//;
    s/^#.*$//;
    next if !$_;

    my $dep=$_;
    my($depdir,$depname,$depsuffix)=parse_filename($dep);
    $depdir= '' if $depdir eq './';
    if ($depsuffix eq 'lib') {
      push(@depends, $dep);
    } elsif ($depsuffix eq 'tco') {
      # If no dir, very likely to be in directory of lbb/liu file
      if (!$depdir) {
	my $ndep="$dir$depname.tco";
	my $sdep="$dir$depname.occ";
	$dep=$ndep if -r $ndep || -r $sdep;
      }
      push(@depends, $dep);
    }
  }
  close(IN);
  return @depends;
}


# Wrap makefile dependencies target: <list of dependencies> according
# to makefile rules: \ at end of line, <TAB> at start of new line.
sub output_makefile_target_line ($$$) {
  my($target, $width, $deps_ref)=@_;
  my $initial="$target: ";
  my $initial_len=length($initial);
  my $line=$initial;
  my $line_len=$initial_len;
  my $str='';
  my $dep;
  foreach $dep (@$deps_ref) {
    my $bit="$dep ";
    my $bit_len=length($bit);
    if (($line_len+$bit_len) > $width) {
      chop $line;
      $str.="$line \\\n";
      $line=" " x $initial_len.$bit; $line_len=$initial_len+$bit_len;
    } else {
      $line.=$bit; $line_len+=$bit_len;
    }
  }
  chop($str.=$line);
  $str."\n";
}


# Wrap makefile rule line (break at spaces, indent by tabs)
sub output_makefile_rule_line ($$) {
  my($width, $rule)=@_;

  return "\t$rule\n";
  # FIXME: Ignored
  my $initial="\t";
  my $initial_len=8;
  my $line=$initial;
  my $line_len=$initial_len;
  my $str='';
  my $bit;
  foreach $bit (split(/ /,$rule)) {
    my $bit_len=length($bit)+1;
    if (($line_len+$bit_len) > $width) {
      chop $line;
      $str.="$line \\\n";
      $line=$initial.$bit; $line_len=$initial_len+$bit_len;
    } else {
      $line.=$bit; $line_len+=$bit_len;
    }
    $line.=" ";
  }
  chop($str.=$line);
  $str."\n";
}


sub expand_dependencies ($$) {
  my($thing, $depends_on_ref)=@_;

  my(%seen_file)=();
  my(@order)=();
  my(@files_to_do)=$thing;
  while(@files_to_do) {
    my $file=shift(@files_to_do);
    my $dep;
    foreach $dep (split(/ /,$depends_on_ref->{$file})) {
      if (!$seen_file{$dep}) {
	$seen_file{$dep}=1;
	# Only add libs depending on libs, except when expanding top level
	# dependencies of a library
	next if $file ne $thing and
	  $file =~ /\.lib$/ and $dep !~ /\.lib$/;
	push(@order, $dep);
	push(@files_to_do, $dep);
      }
    }
  }
  return @order;
}


sub get_lib_inc_real_path ($$$) {
  my($file, $inc_search_ref, $lib_search_ref)=@_;
  return $file if $file =~ m%/%;

  my($dir,$prefix,$suffix)=parse_filename($file);

  my $path_ref= ($suffix eq 'inc') ? $inc_search_ref : $lib_search_ref;
  my $real_path;

  if ($suffix eq 'lib') {
    if ($real_path=path_search("$prefix.lbb", $path_ref, 1) or
	$real_path=path_search("$prefix.liu", $path_ref, 1)) {
      $real_path=~ s%...$%lib%;
    } else {
      $real_path=path_search("$prefix.lib", $path_ref, 1);
      if ($real_path) {
	if (!defined $::glirp_warn{$real_path}) {
	  warn "$::prog_name: Found $file in $real_path without dependency files\n";
	  $::glirp_warn{$real_path}=1;
	}
      }
    }
  } elsif ($suffix eq 'inc') {
    $real_path=path_search($file, $path_ref, 1);
  } else {
    # Otherwise don't check and just use given file name as full path
    $real_path=$file;
  }

  if (!$real_path) {
    warn "$::prog_name: Could not find '$file' in search path: @$path_ref\n";
    return undef;
  }

  $real_path;
}


sub rename_to_native (@) {
  my(@result);
  my $name;
  for $name (@_) {
    my $original_name=$name;
    $name=  "$1.o" if $name =~ /^(.+)\.tco$/;
    $name=~ s%^(.+)/([^/]+)\.lib$%$1/lib$2.a%;
    $name=~ s%^([^/]+)\.lib$%lib$1.a%;
    warn "$::prog_name:     Renamed dependency $original_name to $name\n" if $::verbose and $name ne $original_name;
    push(@result, $name);
  }
  wantarray ? @result : $result[0];
}


sub build_kmakefile ($$$$$$$) {
  my($targets_ref, $kmakefile, $inc_search_ref, $lib_search_ref,
     $extra_inc_search_ref, $extra_lib_search_ref, $cmdline)=@_;

  my $targets_str="@$targets_ref";
  warn "$::prog_name: Building makefile $kmakefile for $targets_str\n" if $::verbose;

  my(%seen_file)=();
  my(%depends_on)=();

  my(%full_path)=();

  my(@order)=();
  my(@files_to_do)=@$targets_ref;

  my $errors=0;
  warn "$::prog_name: Finding dependencies\n" if $::verbose;

  while(@files_to_do) {
    my $file=lc shift(@files_to_do);
    next if $seen_file{$file};

    $seen_file{$file}=1;

    my($dir,$prefix,$suffix)=parse_filename($file);
    my $must_exist_now;
    my $real_path=$full_path{$file};

    my(@dep_files)=();
    
    if ($suffix eq '' || $suffix eq 'tco') {
      # Executable and sep-compiled files depend on source file
      my $src_file="$prefix.occ";
      $must_exist_now=0;
      @dep_files=$src_file;
      $real_path=$file;
      $full_path{$file}=$real_path;
    } elsif ($suffix eq 'occ' || $suffix eq 'inc') {
      $must_exist_now=1;
      # no deps known yet
    } elsif ($suffix eq 'lib') {
      $must_exist_now=1;
      # no deps known yet
    } else {
      warn "$::prog_name: I don't know what to do with file suffix $suffix in '$file'\n";
      next;
    }

    # Find full name...
    if (!defined $real_path) {
      # If must exist, do a search
      if ($must_exist_now) {
	$real_path=get_lib_inc_real_path($file, $inc_search_ref, $lib_search_ref);
	if(!defined $real_path) {
	  $errors++;
	  $real_path=$file;
	}
      }
      $full_path{$file}=$real_path;
      $full_path{$real_path}=$real_path;

      $dir=dirname $real_path;
      $dir= ($dir eq '.') ? '' : "$dir/";
    }

    push(@order, $real_path);

    # If the type is source (occam or include file) and file does exist, 
    # parse the source for dependencies
    if ($suffix eq 'occ' || $suffix eq 'inc') {
      @dep_files=get_occ_file_deps ($real_path) if -r $real_path;
    } elsif ($suffix eq 'lib') {
      my $lbb_path="$dir$prefix.lbb";
      if (-r $lbb_path) {
	warn "$::prog_name:   Adding library dependencies for $file from $lbb_path\n" if $::verbose;
	@dep_files=get_lib_file_deps ($lbb_path);
      }
      my $liu_path="$dir$prefix.liu";
      if (-r $liu_path) {
	warn "$::prog_name:   Adding library dependencies for $file from $liu_path\n" if $::verbose;
	push(@dep_files, get_lib_file_deps ($liu_path));
      }
    }


    # For each dependent file...
    foreach $file (@dep_files) {
      my($dir,$prefix,$suffix)=parse_filename($file);
      my $real_file;
      if($seen_file{$file}) {
	# Skip if already seen and processed
	$real_file=$full_path{$file};
      } else {
	# Otherwise, check if it must exist now
	if ($suffix eq '' || $suffix eq 'tco') {
	  # No for executables or tcos.
	  $real_file=$file;
	} else {
	  # It must, so look for it
	  $real_file=get_lib_inc_real_path($file, $inc_search_ref, $lib_search_ref);
	  if (!defined $real_file) {
	    $errors++;
	    $real_file=$file;
	  }
	}
	$full_path{$file}=$real_file;
	push(@files_to_do, $real_file);
      }
      $file=$real_file; # Alter name in list
    }

    my $txt=($file eq $real_path) ? '': " ($real_path)";
    if ($::verbose) {
      if (@dep_files) {
	warn "$::prog_name:   $file$txt depends on @dep_files\n";
      } else {
	warn "$::prog_name:   $file$txt depends on nothing\n";
      }
    }
    $depends_on{$real_path}="@dep_files";

  } # end while(@files_to_do)

  die "$::prog_name: NOT creating makefile due to errors\n" if $errors;


  # Expand dependencies and rename foo.tco->foo.o and foo.lib -> libfoo.a
  my(%native_depends_on)=();
  my(@native_order)=();
  {
    warn "$::prog_name: Expanding dependencies and renaming\n" if $::verbose;

    my $file;
    foreach $file (@order) {
      my($dir,$prefix,$suffix)=parse_filename($file);
      my $original_file=$file;

      $dir= '' if $dir eq './';
      my(@dep_files)=split(/ /,$depends_on{$file});
      
      if ($suffix eq '' || $suffix eq 'lib' || $suffix eq 'tco') {
	warn "$::prog_name:   Target $file directly depends on @dep_files\n" if $::verbose;
	my(@full_dep_files)=expand_dependencies($file, \%depends_on);
	warn "$::prog_name:   Target $file fully depends on @full_dep_files\n" if $::verbose;

	@dep_files=();
	my $dep;
	foreach $dep (@full_dep_files) {
	  my $new_name=rename_to_native($dep);
	  push(@dep_files, $new_name);
	  warn "$::prog_name:     Added $new_name to dependencies\n" if $::verbose;
	}

	$file=rename_to_native($file);

	# Only make targets of executables, libraries and objects
	push(@native_order, $file);

      } else {
	warn "$::prog_name:   Renaming dependencies for $file\n" if $::verbose;
	@dep_files=rename_to_native(@dep_files);	
	$file=rename_to_native($file);
      } # if ($suffix ...)

      $native_depends_on{$file}="@dep_files";
      warn "$::prog_name:   Renamed $original_file to $file\n" if $file ne $original_file and $::verbose;
    }
  }

  # Rename targets
  my(@native_targets)=();
  {
    my $target;
    foreach $target (@$targets_ref) {
      my($tdir,$tfile,$tsuffix)=parse_filename($target);
      $tdir='' if $tdir eq './';
      if ($tsuffix eq 'lib') {
	push(@native_targets, "${tdir}lib${tfile}.a");
      } else {
	push(@native_targets, $target);
      }
    }
  }


  undef %depends_on;
  undef @order;

  return if $::dryrun;

  # OUTPUT Makefile

  {
    warn "$::prog_name: Creating makefile $kmakefile\n" if $::verbose;
    
    open(OUT, ">$kmakefile") or die "$::prog_name: Cannot create $kmakefile - $!\n";
    my $login=getlogin || (getpwuid($<))[0] || "unknown";
    my $date=localtime(time);
    
    print OUT "# Makefile built by $::prog_name for $login on $date\n";
    print OUT "# with '$::prog_name $cmdline'\n";
    print OUT "#\n";
    print OUT "KROC=kroc\n";
    print OUT "ILIBR=ilibr\n";
    print OUT "AR=ar\n";
    print OUT "RANLIB=ranlib\n";
    print OUT "DEL=rm -f\n\n";
    
    print OUT "# Default - build all targets\n";
    print OUT "all: @native_targets\n\n";
    
    print OUT "clean:\n\t-\$(DEL) @native_targets *.tco *.o *.lib *.a *.s *.kt* *~\n\n";
    
    my $nl=0;
    my $file;
    foreach $file (@native_order) {
      my($dir,$prefix,$suffix)=parse_filename($file);
      $dir= '' if $dir eq './';
      my(@dep_files)=split(/ /,$native_depends_on{$file});
      if (@dep_files) {
	my $rule=get_rule($file, \%native_depends_on, \@dep_files,
			   $extra_inc_search_ref, $extra_lib_search_ref);
	print OUT "\n" if $nl;
	print OUT output_makefile_target_line($file, 75, \@dep_files);
	print OUT output_makefile_rule_line(75, $rule) if $rule;
	$nl=1;
      }
    }

    close(OUT);
    warn "$::prog_name: Done\n" if $::verbose;
  }
}


sub main {
  my(@args)=@_;

  my(@search_path)=split(/:/, $ENV{'PATH'});
  die "$::prog_name: Could not find 'kroc' program in search path\n"
    unless path_search('kroc', \@search_path, 0);

  my $path;
  chop($path=`kroc --incpath`);
  my(@inc_search)=(split(/ /,$path));
  # Remove current directory, always first
  shift(@inc_search);

  chop($path=`kroc --libpath`);
  my(@lib_search)=(split(/ /,$path));
  # Remove current directory, always first
  shift(@lib_search);

  my $cmdline="@args";

  my $usage=0;
  my $outfile='';
  my(@newargs)=();
  my(@extra_inc_search);
  my(@extra_lib_search);
  while ($_=shift @args) {
    last if $_ eq '--';
    if (/^(-.+)$/) {
      my $flag=$1;
      if ($flag eq '-v' || $flag eq '--verbose') {
	$::verbose++;
      } elsif ($flag eq '-n' || $flag eq '--dryrun') {
	$::dryrun++;
	$::verbose=1;
      } elsif ($flag eq '-p' || $flag eq '--print') {
	$outfile='-';
      } elsif (/^-I(.+)$/) {
	push(@extra_inc_search,$1);
      } elsif (/^-L(.+)$/) {
	push(@extra_lib_search,$1);
      } elsif (/^-o(.+)$/) {
	$outfile=$1;
      } else {
	warn qq{$::prog_name: Unrecognised option: "$flag"\n};
	$usage=1;
      }
    } else {
      push(@newargs, $_);
      last;
    }
  }
  push(@newargs, @args);

  $usage=1 unless @newargs;
  
  warn <<"EOT" and exit 1 if $usage;
KROC Makefile Generator version $::version
Usage: $::prog_name [options] [executables or libraries]
Options:
  -ooutput         Output makefile to this file (Default is executable.kmf)
  -p --print       Print output makefile to terminal
  -Idirectory      Search 'directory' for occam includes
  -Ldirectory      Search 'directory' for occam/C libs
  -n --dryrun      Don't build a makefile but show what would be done
  -v --verbose     Show status information during run
  -h --help        Print this message and exit

PLEASE NOTE:
  This is an early version of this tool and it probably does not
  handle building libraries correctly (not tested much).
EOT


  unshift(@inc_search, @extra_inc_search) if @extra_inc_search;
  unshift(@lib_search, @extra_lib_search) if @extra_lib_search;

  if ($::verbose) {
    warn "$::prog_name: Search paths being used are:\n";
    warn "  Native libraries (libfoo.a):\n";
    warn "  . @lib_search\n";
    warn "  occam libraries/#USEs (foo.lib) and #INCLUDEs (foo.inc); C #includes (foo.h):\n";
    warn "  . @inc_search\n";
  }


  my(@targets)=();
  {
    my(%seen_target);

    my $file;
    foreach $file (@newargs) {
      my($dir,$prefix,$suffix)=parse_filename($file);
      $dir='' if $dir eq './';

      my $target="$prefix.$suffix";
      if ($suffix eq '' and
          (-r "$dir$prefix.lbb" || -r "$dir$prefix.liu")) {
        $suffix='lib'; $target="$prefix.$suffix";
      } elsif ($suffix eq 'occ') {
        $suffix=''; $target=$prefix;
      } elsif ($suffix eq '') {
        $target=$prefix;
      } elsif ($suffix eq 'lib') {
        # Nothing to do
      } else {
	warn "$::prog_name: $file: Unknown file type '$suffix', ignoring\n";
	next;
      }

      if ($target ne $file) {
	warn "$::prog_name: Assuming target $file meant target $target\n";
      }

      if ($seen_target{$target}) {
	warn "$::prog_name: $file: Duplicated target $target\n";
	next;
      }
      $seen_target{$target}=1;
      push(@targets, $target);
      if (!$outfile) {
	$outfile="$prefix.kmf";
	warn "$::prog_name: Outputing to makefile $outfile\n";
      }
    }
  }
  exit 0 unless @targets;

  build_kmakefile(\@targets, $outfile, \@inc_search, \@lib_search,
                  \@extra_inc_search, \@extra_lib_search, $cmdline);
}



=head1 NAME

kmakef - OFA KROC makefile builder

=head1 SYNOPSIS

  kmakef [options] executables or libraries

=head1 DESCRIPTION

I<kmakef> is a makefile-builder program for the occam For All (oFA)
project's KRoC compilation system.  It scans given source files or
libraries and creates a prototype makefile which can be used to build
the system, understanding I<KRoC>'s dependencies.

If a library is being constructed e.g. I<foo>C<.lib>, a library
intermediate file is required to list the modules inside it.  This is
given in either a I<foo>C<.lbb> or I<foo>C<.liu> file and contains
just a list of filenames, one per line.


=head1 OPTIONS

=over 4

=item -oI<FILE>

Output the makefile to I<FILE> (Default is I<executable>.kmf).

=item -p, --print

Print the resulting makefile rather than store it in a file.

=item -II<DIRECTORY>

Add directory I<DIRECTORY> to the list of directories to search for
occam #INCLUDEs.

=item -LI<DIRECTORY>

Add directory I<DIRECTORY> to the list of directories to search for
occam and C libraries.

=item -n, --dryrun

Don't build a makefile but show what would be done.

=item -v, --verbose

Show status and debugging information during the program run.

=item -h --help

Print a help message and exit.

=back

=head1 EXAMPLE

For a program, given a single source file F<hello.occ>

  % kmakef hello.occ 
  kmakef: Assuming target hello.occ meant target hello
  kmakef: Outputing to makefile hello.kmf

which results in F<hello.kmf> containing:

  # Makefile built by kmakef for ... on ...
  # with 'kmakef hello.occ'
  #
  KROC=kroc
  ILIBR=ilibr
  AR=ar
  RANLIB=ranlib
  DEL=rm -f

  # Default - build all targets
  all: hello

  clean:
	-$(DEL) hello *.tco *.o *.lib *.a *.s *.t *~

  hello: hello.occ
	$(KROC) hello.occ -o hello


=head1 SEE ALSO

See also L<kroc>.

=head1 AUTHOR

Dave Beckett E<lt>I<D.J.Beckett@ukc.ac.uk>E<gt>.

=head1 COPYRIGHT

Copyright 1996,1997 Dave Beckett.

=cut
