#!/usr/bin/perl -w
{
  $program_id = "build.pl v 1.2a  23 Oct 2009";
#  23 Oct 09.  WJS	v 1.2a
#	Disabled attempt to check for read errors, since it seemed to
#     fail for some fleetlink (linux) make files.
#   7 Jun 09.  WJS	v 1.2
#	Bug fix: build.pl clean did not work.  Soln by recoding
#     arg parsing so as not to depend on perl -s switch
#     Note: there are references to a -force switch in the comments
#     but not in the code or the build.doc file.  I suspect these were
#     from before the  touch build-env.pl  stuff but I'd want to dig
#     more before removing them.  Relevant since this code revision will
#     diagnose -force as an unknown switch, if I remember what perl -s
#     does (not that I looked it up to see, mind you)
#  27 Mar 08.  WJS	v 1.1
#	No longer anything special about src/methods/makefile.aamethods
#     It's gone on OOservers and could be gone on data servers if we made
#     def a symbolic link to defgb
#  27 Apr 07.  WJS	v 1.0b
#	Try to get rid of bogus "Could not read" error on linux boxes
#   9 Jun 05.  WJS	v 1.0a
#	Bug fix of a problem if 2 "things" started w/same string
#     (eg, defgb & def)
#	Add program id string; put it in error msgs

###############################

#
#  Master Makefile for Otheroption server [or data server]
#   - note that only makefile.extension files are 'make'd
#   - note that this is a perl script, not a "Master Makefile".  We
#     do certain primitive make operations in here, such as being sure
#     that in a complete build, the library is made first.  To be "real",
#     there would be one system-wide make file and no shell scripts.
#  NOTE: this file artificially updates build-env.pl.  Further, it expects
#     all dependencies in all make files to depend on build-env.pl.  Thus,
#     everything in a make file should be made on every invokation of build.pl

#		SWITCHES
#  Accepts 2 switches (before non-switch arguments, if any)
#    -cleanfirst/-cleanonly.  Either switch calls the selected make files
#	with a target of "clean".  -cleanfirst will then call the selected
#	make files without the "clean" target.  -cleanonly is the equivalent
#	of an argument of "clean", which is accepted for backward compatibility
#	"Works" as well as the make files' clean targets interact w/the
#	dependencies in the make files.  -force conflicts with -cleanonly.

#		ARGUMENTS
#  If called w/no arguments, will make "everything".  Otherwise,
#  accepts list of arguments, which are processed in order.  An argument can
#  satisfy several of the criteria below - the criteria are applied in order
#  (eg, can't have a method called 'clean'; will chose subdir defgb even if
#  makefile.defgb exists)
#  If argument is:
#    1.  clean   (opt-build clean)
#		see -cleanonly switch
#    2.  subdirectory (opt-build lib) 
#		make only makefiles in that subdirectory of optsrc.
#		Note that this does NOT necessarily make all target files 
#		in the argument subdirectory of $jgofs_root (Example: 
#		opt-build methods does not build methods/defgb because
#		makefile.defgb is typically in optsrc/defgb, not optsrc/methods)
#    3.  target file (opt-build list [opt-build makefile.list])
#		will look for a make file named makefile.target.  There is
#		no way to deal w/makefile.target found in > 1 directory.
#		Can specify leading "makefile." to allow discrimination 
#		between subdirs and makefiles within that subdir
#    4.	 target (opt-build methods/def)
#		calls all make files in methods with target as an argument.
#		Directory spec required; * (NOT escaped - let shell do
#		expansion) allowed.
#		"Works" as long as make files code targets as "../dir/target"
#		Neither clean switch is allowed w/target (clean presumably 
#		cleans all targets in a make file, which is more than 
#		target would make)
#

  $TRUE = 1;
  $FALSE = 0;

#   "Magic" string ID'ing makefiles this routine will make.  To be more
#   exact, such makefiles' file specs must end in /makefile.$STRING, where
#   $STRING is a non-empty string.
  $id_string = "makefile.";

  $working_dir = `pwd`;
  chomp $working_dir;
  $this_file = $0;

#   Use cd command to get directory w/o shell directory shortcuts and w/o
#   the . and .. directories.  Don't know how to do it easily otherwise
  ($dir,$file) = ($this_file =~ /^(.*)\/(.+)$/);
  if ($file) {
#     Presumably $dir must be defined if $file is, so testing $dir is
#     asking whether or not it's empty
    $jgofs_root = ($dir) ? `(cd $dir; pwd)` : "";
  } else {
#     Presumably $dir is not defined, or we'd have a non-empty dir spec w/an
#     empty file name.  Don't think this is worth a diagnostic
    $jgofs_root = $working_dir;
    $file = $this_file;
  }
  chomp $jgofs_root;

  $opt = ($file =~ /^opt-/);

  $build_env_file_name = $jgofs_root . "/";
  $opt && ($build_env_file_name .= "opt-");
  $build_env_file_name .= "build-env.pl";
#   Next env var should be set by build-env.pl to point to build-env.pl, and
#   should be in every make file in order to force builds
  $config_file_env_var = "CONFIG_FILE";

  $source_root = $jgofs_root . "/";
  $opt && ($source_root .= "opt");
  $source_root .= "src";

#   build-env.pl is a main program of its own, so make sure it
#   doesn't operate on our argument list
#   Note also that for build-env.pl to "know its name", it uses
#   global variable $build_env_file_name.  $0 is build.pl's name when
#   build-env.pl is require'd
  @arglist = @ARGV;
  @ARGV = ();
  require $build_env_file_name;
#   Check coordination of CONFIG_FILE env var between here and build-env.pl
  ($ENV{$config_file_env_var} eq $build_env_file_name) ||
	&bad_out ("\$ENV{$config_file_env_var} ne $build_env_file_name\n");

#   Find and get rid of the "clean" parameters
  $switch_processing = $TRUE;
  foreach (@arglist) {
    if (  /^-/ || ($_ eq "clean")  ) {
      $switch_processing || 
	&bad_out("Switches/clean parameter must be at start of command args\n");
      if (($_ eq "-cleanonly") || ($_ eq "clean"))  {
	$cleanonly = 1;
      } elsif ($_ eq "-cleanfirst") {
	$cleanfirst = 1;
      } else {
	&bad_out ("Unknown switch: $_\n");
      }
      shift @arglist;
    } else {
      $switch_processing = $FALSE;
    }
  }
  $cleanonly && $cleanfirst && 
    &bad_out 
      ("Conflicting switches -cleanfirst and -cleanonly (or argument clean)\n");

  $clean = ($cleanonly || $cleanfirst);
  $build = ! $cleanonly;

#  make the stuff in lib, first - others depend on these
#  (tilde stuff apparently related to editor backup version stuff for
#   some editor in use when original build being developed  WJS Jun 04)
#  (Apparently some ls's have a -B to ignore these backup files.  Might
#   someday use that instead of grep.  WJS Mar 08)
  @list = `ls -1 $source_root/lib/$id_string* | grep -v "~"`;
#
#  Add other subdirectory stuff ONE LEVEL under $source_root
#     
  $list_command = "  ls -1 $source_root/*/$id_string* " .
		'| grep -v "/lib/" ' .
		'| grep -v "~"';
  push (@list, `$list_command` );

  (@list == 0) && 
	&bad_out ("Cannot find any files of the form " .
	     "$source_root/*/$id_string*\n");
  foreach (@list) {
    chomp;
  }

  system "touch $ENV{$config_file_env_var}";

  if (@arglist == 0) {
    &do_make("part of full make","",@list);
  } else {
    foreach (@arglist) {
      ($_ eq "clean") && next;

      if (m"/") {
#	  Args that are targets must include a directory spec
	$clean &&
		(warn "Cannot use clean switch with target argument\n") && 
		next;
	$type_of_make = "target";
      } elsif (/^$id_string/) {
#	  Args that start with "makefile." are makefiles.  Strip off
#	  magic string and treat them like "normal" args that aren't subdirs
	($_) = /^$id_string(.+)$/;
	$_ ||
		(warn "No target in argument $id_string\n" && 
		 next);
	$type_of_make = "requested makefile";
      } else {
#	    Arg is a subdir if we can find string "optsrc/$arg" in
#	    list of all make files.  Trailing slash deals w/things like
#	    def vs defgb.
	  $temp = "^$source_root/$_/";		# Can NOT use $_ in grep call
	  @sub_list = grep(m"$temp",@list);
	  $type_of_make = (@sub_list == 0) ?
				"requested makefile" :
				"part of subdirectory make";
      }

      if ($type_of_make eq "target") {
#	  See coding in make files.  Handle case if arg has initial /
	$target = m"^/" ? "../..$_" : "../../$_";
	@sub_list = @list;
      } elsif ($type_of_make eq "requested makefile") {
	$target = "";
	$temp = "$id_string$_\$";		# Can NOT use $_ in grep call
	@sub_list = grep( m"$temp", @list);
	(@sub_list > 1) &&
		(warn "More than one $id_string$_ file; namely \n" .
		     join ('\t',@sub_list) . "\n")			    &&
		 next;
      } elsif ($type_of_make eq "part of subdirectory make") {
	$target = "";
      } else {
	&bad_out ("Internal coding error - unknown value for \$type_of_make\n");
      }

      if (@sub_list == 0) {
	warn "Cannot find appropriate make files\n" .
	     "\tThere are no $id_string* in $source_root/$_ nor \n" .
	     "\tare there any $id_string$_ files in $source_root/*\n";
      } else {
	&do_make($type_of_make,$target,@sub_list);
      }
    }
  }
  exit;
}

sub do_make
{
  my ($type_of_make,$target,@files) = @_;
  my ($dir,$file,$message);
  my ($n_makefiles_for_target,$any_force_worked,$target_exists_as_file);

#   Set up to check that for target makes, target appears in exactly 1 make file
  $n_makefiles_for_target = 0;
#   Next 2 variables used to try to figure out if a target is up-to-date or
#   is not a real target.  1) Read comments below   2) Code using these variables
#   is NOT guaranteed to work, but failure amounts to deceptive message
  $target_exists_as_file = 0;

  foreach (@files) {

    ($dir,$file) = m"^(.+)/($id_string.+)$";
    ($dir && $file) || &bad_out ("Bad make file spec $_\n");
    
    chdir $dir;

    if ($clean) {
      print "\n ... cleaning $_\n";
      $status = system("make -f $file clean");
    }

    if ($build) {


      if ($target) {
#	  make -q -f file target on globec returns 0 if target exists in file but is 
#	  up-to-date, 1 if target exists in file but needs updating, and 2 (w/output
#	  to stderr) if target does not exist in file.  Therefore, we should be able
#	  to go through all make files until we get a 0 or 1 back, and then act
#	  accordingly.  However, 1) target might appear in >1 make file  2) make
#	  exit statuses are not explicitly defined in man page (or O'Reilly book), so
#	  presumably they could differ.  Therefore, go through all make files and
#	  report results accordingly.  Sorry about hex/decimal and shift stuff in
#	  constants.
#		More:
#	  1) verified that /usr/ccs/bin/make on globec does not return 2
#	  2) found out that all makes return 0 if target happens to be an
#	     existing file and make file does NOT contain target.  For 
#	     example, after a cd optsrc/methods,
#			make -q -f makefile.stats ../../methods/defgb
#	     returns 0.  Best way around this is to change target names
#	     (after all, the don't HAVE to match the target file names).  However,
#	     at the moment, they all do)
#	  3) May be able to parse output (globec make: "nothing to do for defgb"
#	     (if target not in file) vs "defgb up-to-date" (if target IS in file)
#	     However, output varies between makes.  Would have to prepare 2
#	     test cases of each type and diff them (to find out where the target
#	     names appear in each message).  Then somehow make that diff into
#	     a template into which we could insert real target names.
#		Therefore: gave up on distinguishing between up-to-date
#	     targets and targets that appear in no make files.  This is
#	     another reason to use -force
	$status = system ("make -q -f $file $target 2> /dev/null");
	$target_exists_as_file || ($target_exists_as_file = (-e $target));
	(($status == 512) || ($status == 0)) && next; 
	($status == 256) || 
		&bad_out ("Uncoded-for return status ($status) from make\n");
	$n_makefiles_for_target++;
	$thing_being_made = $target;
	$description = "using $_";
      } else {
	$thing_being_made = $_;
	$description = $type_of_make;
      }

      print "\n ... making $thing_being_made ($description)\n";

      $status = &check_macro($_,"#",$config_file_env_var);
      if ($!) {
	$problem = ($status) ? $status : "access";
	warn "Could not $problem $_.  \$! = $!\n";
      } elsif ($status) {
	warn 
	 " *** make file $_ does not contain appropriate\n" .
	 " *** macro name ($config_file_env_var) to force makes\n";
      }

      system ("make -f $file $target");

    }

    chdir $working_dir;

  }

  if ($target) {
    if ($n_makefiles_for_target == 0) {
      $message = " *** $target not found in any make file";
      if ($target_exists_as_file) {
        $message .= " or an up-to-date file named $target exists ";
      }
      warn "$message\n";
    } elsif ($n_makefiles_for_target > 1) {
      warn " *** $target found in > 1 make file\n";
    }
  }

  return;
}

sub check_macro
{
  my ($file,$comment_char,$macro) = @_;

#   Check that file $file contains string of form ${macro} in some
#   non-comment line.  Normal return is empty string.  Return $macro if macro not found.
#   If I/O error, return "open", "read", or "close" to indicate where problem
#   occurred.  $! is an implicit return and is always set by this routine (0 = OK).
#   NB: if comment character is special to perl regex's, it must be backslashed

  my ($rec,$save_dollar_bang);

  $! = 0;

  open(FILE,"< $file") || (return "open");

  $! = 0;

  while ( $macro && (defined ($rec = <FILE>))  ) {
    ($rec) = split (/$comment_char/,$rec);
    $rec && ($rec =~ /\$\{$macro\}/) && ($macro = "");
    $! = 0;
  }

#   Loop seemed to end normally but set $! to "bad file descriptor" on fleelink
#   (linux) ... but not for all files?!  Seems to be no way to test for 
#   read error?!
#	  $! && ($save_dollar_bang = $!);
  $! = 0;
  close FILE;
  $save_dollar_bang && ($! = $save_dollar_bang) && return "read";
  $! && return "close";

  return $macro;
}

sub bad_out
{
  die (@_,"$program_id\n");
}
