#!/usr/local/bin/perl -w
#
# (c) 1998-2001 Jan-Henrik Haukeland, <hauk@tildeslash.com>
#

=head1 NAME

 mmake - generate a Java Makefile

=head1 SYNOPSIS

 mmake [ -d | -v ]

=head1 DESCRIPTION

This program will generate a Makefile for Java source files. Use the
I<-d> option to accept all defaults.

After running mmake, you will obtain a Makefile in the directory from
where you started the program. The Makefile will handle java files in
the current directory and in any sub-directories.

Use the generated Makefile with mmake as follows:

To compile Java files just type B<make>. It's also possible to run
make with one of the following targets: I<doc, clean, help, jar,
install, uninstall, tags and depend> Where 'make doc' runs javadoc on
the source files, it will only work for files in a package. The
command 'make clean' removes class files and other temporary
files. The command 'make jar' creates a jar file with all class files
(and other files of your choice, see the JAR_OBJS variable in the
Makefile). The command 'make install' will install a jar file, class
files and any shell wrappers you have made. (A shell script must have
the extension .sh to be installed). Use 'make uninstall' to remove
installed files. The command 'make help', shows a help text with
available targets. The command 'make tags' will generate a tag file
for Emacs. And finally the command 'make depend' creates a dependency
graph for the class files. (The dependency graph will be put in a file
called I<makefile.dep>, which is included in the Makefile)

You don't have to run mmake each time you add a new java file to your
project. You can add as many new java files as you like, the Makefile
will find them. This is the case as long as you don't add a new
package. In that case, you must either run mmake again or update the
PACKAGE variable in the Makefile. This is because the Makefile uses
this variable to find directories with java files.

The program mmake is able to create a dependency graph for your java
files. To do this, it needs the I<jikes> compiler from IBM. Get jikes
from B<http://www.ibm.com/java/tools>. You would probably be more
content with jikes anyhow, since it is much faster than javac. To
create a dependencies graph, do a I<make clean> before running I<make
depend>.


=head1 A NOTE ON INSTALLATION

The Makefile created with mmake will do a fair job installing the
different files that makes up your system. It uses the following
Makefile variables when it conducts the install routine:

=over 4

=item * 

PREFIX

=item * 

CLASS_DIR

=item * 

JAR_DIR

=item * 

DOC_DIR

=item * 

SCRIPT_DIR

=back

=head2 PREFIX

This variable will be prepended to all other directory variables
above. It is used for grouping the other directories into one root
directory. If you don't want that, you may simply set the variable to
an empty string in the Makefile. If the variable is empty you could
still use it on the command line when you run make, for instance for a
one-shoot installation like: B<make PREFIX=/local/myproject/ install>

=head2 CLASS_DIR

This variable denotes the top directory from where all class files
will be installed. Its default value is B<classes>, which I believe is
a good value. B<Note:> If you I<don't> want to install any class files
(because you are, for example, only going to use a jar file), set this
variable to an empty string and no class files will be installed. 

Resource files will also be installed below this directory if such
files are present in a package structure. This is useful if you are
using e.g. ResourceBundles to Localize your application and have your
property files in it's own directory in the package structure.

=head2 JAR_DIR

This variable tells the Makefile where to install the jar file. The
default value is B<lib>, which is also a good default value.

=head2 DOC_DIR

When you run javadoc, all the html files will be put into this
directory. Its default value is B<doc/api-docs>. You should probably
keep that name, but then again, you may change it as you like.

=head2 SCRIPT_DIR

The Makefile uses this variable to install any shell wrapper-scripts
that you have created. If you write an application, it is always nice
for the user that you provide a wrapper script to start the
application. Its default value is B<bin>. (The Makefile will only
install shell-scripts that has the extension .sh. The mmake script
will tell the Makefile where to look for shell-scripts)


=head2 INSTALLATION SUMMARY

If you keep the default values you will get an installation tree that
looks like this:

 `-- PREFIX
     |-- bin
     |-- classes
     |   `-- package <--- Example of a sub-directory
     |       |-- sub-package1
     |       |-- sub-package2
     |       `-- sub-package3
     |-- doc
     |   `-- api-docs
     `-- lib


=head1 USING THE C-PREPROCESSOR

This is a excellent tool for managing projects with several different
versions. The idea behind using the C preprocessor with Java is to
better manage different versions more easily. This is done by using
CPP conditional statements in the source files. I would strongly
advise you not to use CPP to redefine the Java language itself.

To use the C preprocessor together with Java, you can change the name
of the source files that you want to preprocess -- from
<filename>.java to <filename>.xjava. The Makefile has a rule to build
.class files from .xjava files.

It is B<not> necesarry to change every file from .java to .xjava. The
Makefile will work well and consistently in an environment of both
.java and .xjava files. (E.g. 'make clean' will only remove .java
files that were created from a .xjava file. Other java files will, of
course, I<not> be removed.)

You can now use cpp Conditionals in Your Java-code, for example, as
follows:

    #ifdef JAVA1_1
       [code1]
    #else
       [code2]
    #endif

The JAVA1_1 label in the above example is tested against the
VERSION variable in the Makefile. That is, if the VERSION variable is
JAVA1_1, then [code1] would be compiled and [code2] left out. Likewise,
if VERSION is something else than JAVA1_1, then [code2] would be compiled and
[code1] left out of the resulting .class file.

=head1 NOTES

mmake will give you I<one> Makefile for managing your Java files.
Although it's easy to setup and use mmake in a recursive makefile
context, you don't want to do that. To see why, read the excellent
article: B<Recursive Make Considered Harmful> at
I<http://www.canb.auug.org.au/~millerp/rmch/recu-make-cons-harm.html>

=head1 DEPENDENCIES

mmake will need the following:

=over 4

=item * 

Perl 5.x

=item * 

Gnu make

=item * 

Gnu xargs (recommended)

=back

=head1 AUTHOR

Jan-Henrik Haukeland <hauk@tildeslash.com>

=cut

use strict;
use vars qw($opt_d $opt_v);
use Getopt::Std;
require 5.000;                   # Need this perl version at least

# Prototypes
sub getline($$);
sub getdirline($$);
sub getpreviewline($$$);
sub do_get();
sub do_find($$);


my $REVISION= sprintf("%d.%02d", q$Revision: 1.8 $ =~ /(\d+)\.(\d+)/);

my $VERSION= "2.2.1";            # The program version
(my $PROG = $0)=~   s,.*/,,;     # This Program name (usually 'mmake')
my $M=             "Makefile";   # The Java Makefile
my @packages=      ();           # Array holding packages, i.e. subdirectories 
my @scripts=       ();           # Array holding shell-script directories 
my @resources=     ();           # Array holding resource files
my $toplevel=      "";           # Defined if toplevel java files

# Parse command line options
getopts("dv") || die "Usage: $PROG [ -d | -v ]\n";

if ( defined $opt_v ) {
  print "This is mmake, version $VERSION\n";
  exit(0);
}


# ---------------------
# Assign macro defaults
# ---------------------
my $javac=        "jikes"; 
my $jflags=       "";
my $javadoc=      "javadoc";
my $jdocflags=    "-version -author";
my $jar=          "jar";
my $jarflags=     "cvf0";
my $jarfile=      "";
my $prefixdir=    "";
my $docdir=       "doc/api-docs";
my $jardir=       "lib";
my $classdir=     "classes";
my $bindir=       "bin";
my $cpp=          "cpp";
my $cppflags=     "-C -P";


if (-t and ( !$opt_d )) {
    # Let the user override the defaults
    print "Give Makefile variables or just type [enter] for default\n\n";
    $javac=         getline("JAVAC",   $javac);
    $jflags=        getline("JAVAC flags", $jflags);
    $javadoc=       getline("JAVADOC", $javadoc);
    $jdocflags=     getline("JAVADOC flags", $jdocflags);
    $jar=           getline("JAR", $jar);
    $jarflags=      getline("JAR flags", $jarflags);
    $jarfile=       getline("JAR File name (e.g. foobar.jar)", $jarfile);

    $prefixdir=     getdirline("PREFIX dir. (Will be prepended to other ".
			       "install dir)",  $prefixdir);
    $docdir=        getpreviewline("INSTALL dir. for javadoc html-files.",
				   "$prefixdir$docdir", $docdir);
    $classdir=      getpreviewline("INSTALL dir. for class files", 
				   "$prefixdir$classdir", $classdir);
    $bindir=        getpreviewline("INSTALL dir. for shell-scripts",
				   "$prefixdir$bindir", $bindir);
    $jardir=        getpreviewline("INSTALL dir. for the jar file ",
				   "$prefixdir$jardir", $jardir);

    if( getline("Use CPP [y|n] ?", "no") =~ /^y/i ) {
	$cpp=      getline("CPP", $cpp);
	$cppflags= getline("CPP flags", $cppflags);
    }

    print "\n";
}


# Locate the java files/packages
do_get();
die "No java source files found\n" unless @packages or $toplevel;

# If an old Makefile exists, rename it
if (-f $M) {
    rename($M, "$M.old") or
      die "$PROG: Cannot rename local Makefile.\n";
}

# Then create the new makefile
open(MAKEFILE, ">$M") || die "$PROG: Cannot create '$M': $!\n";


print MAKEFILE <<EOT;
#
# Makefile created at @{[scalar localtime]}, by $PROG
#

# Programs (with common options):
SHELL		= /bin/sh
RM              = rm -f
MV              = mv -f
SED		= sed
ETAGS		= etags
XARGS		= xargs
CAT		= cat
FIND            = find
CPP		= $cpp $cppflags

INSTALL         = install
INSTALL_PROG    = \$(INSTALL) -m \$(MODE_PROGS)
INSTALL_FILE    = \$(INSTALL) -m \$(MODE_FILES)
INSTALL_DIR     = \$(INSTALL) -m \$(MODE_DIRS) -d

# Install modes 
MODE_PROGS      = 555
MODE_FILES      = 444
MODE_DIRS       = 2755

# Build programs
JAVAC           = $javac
JAVADOC         = $javadoc
JAR             = $jar

# Build flags
JAVAC_FLAGS     = $jflags
JAVADOC_FLAGS   = $jdocflags
JAR_FLAGS       = $jarflags
JIKES_DEP_FLAG	= +M

# ------------------------------------------------------------------- #

# Prefix for every install directory
PREFIX		= $prefixdir

# Where to start installing the class files. Set this to an empty value
#  if you dont want to install classes
CLASS_DIR	= \$(PREFIX)$classdir

# The directory to install the jar file in. Set this to an empty value
#  if you dont want to install a jar file
JAR_DIR	        = \$(PREFIX)$jardir

# The directory to install html files generated by javadoc
DOC_DIR         = \$(PREFIX)$docdir

# The directory to install script files in
SCRIPT_DIR	= \$(PREFIX)$bindir

# ------------------------------------------------------------------- #

# The name of the jar file to install
JAR_FILE        = $jarfile

# 
# The VERSION variable below should be set to a value 
# that will be tested in the .xjava code. 
# 
VERSION		= CHANGE_ME

# ------------------------------------------------------------------- #

EOT


# Print the packages 
print MAKEFILE "
# Packages we should compile
PACKAGES = ",
  @packages ? ("\\\n\t" . join(" \\\n\t", @packages)) : "",
  "\n\n";

# Print the resource "packages"
print MAKEFILE "
# Resource packages
RESOURCES = ",
  @resources ? ("\\\n\t" . join(" \\\n\t", @resources)) : "",
  "\n\n";

# Print the script dir list
print MAKEFILE "
# Directories with shell scripts
SCRIPTS = ",
  @scripts ? ("\\\n\t" . join(" \\\n\t", @scripts)) : "",
  "\n\n";


print MAKEFILE <<EOT;
# ------------------------------------------------------------------- #

# A marker variable for the top level directory
TOPLEVEL	:= .

# Subdirectories with java files:
JAVA_DIRS	:= \$(subst .,/,\$(PACKAGES)) \$(TOPLEVEL)

# Subdirectories with only resource files:
RESOURCE_DIRS	:= \$(subst .,/,\$(RESOURCES))

# All the .xjava source files:
XJAVA_SRC	:= \$(foreach dir, \$(JAVA_DIRS), \$(wildcard \$(dir)/*.xjava))

# All the xjava files to build
XJAVA_OBJS	:= \$(XJAVA_SRC:.xjava=.java)

# All the .java source files:
JAVA_SRC	:= \$(foreach dir, \$(JAVA_DIRS), \$(wildcard \$(dir)/*.java))
JAVA_SRC	:= \$(XJAVA_OBJS) \$(JAVA_SRC)

# Dependency files:
DEPEND_OBJS	:= \$(JAVA_SRC:.java=.u)

# Objects that should go into the jar file. (find syntax)
JAR_OBJS	:= \\( -name '*.class' -o -name '*.gif' -o -name "*.au" \\
		       -o -name '*.properties' \\)

# The intermediate java files and main classes we should build:
JAVA_OBJS	:= \$(XJAVA_OBJS) \$(JAVA_SRC:.java=.class)

# Resource files:
#  Extend the list to install other files of your choice
RESOURCE_SRC	:= *.properties *.gif *.au
#  Search for resource files in both JAVA_DIRS and RESOURCE_DIRS
RESOURCE_OBJS	:= \$(foreach dir, \$(JAVA_DIRS) \$(RESOURCE_DIRS), \\
		     \$(wildcard \$(foreach file, \$(RESOURCE_SRC), \\
		     \$(dir)/\$(file))))

# All the shell scripts source
SCRIPT_SRCS 	:= \$(foreach dir, \$(SCRIPTS), \$(wildcard \$(dir)/*.sh))
# All shell scripts we should install
SCRIPT_OBJS    	:= \$(SCRIPT_SRCS:.sh=)

# All the files to install into CLASS_DIR
INSTALL_OBJS	:= \$(foreach dir, \$(JAVA_DIRS), \$(wildcard \$(dir)/*.class))
# Escape inner class delimiter \$
INSTALL_OBJS	:= \$(subst \$\$,\\\$\$,\$(INSTALL_OBJS))
# Add the resource files to be installed as well
INSTALL_OBJS	:= \$(INSTALL_OBJS) \$(RESOURCE_OBJS)


# ------------------------------------------------------------------- #


define check-exit
|| exit 1

endef


# -----------
# Build Rules
# -----------

%.java: %.xjava
	\$(CPP) -D\$(VERSION) \$< \$@

%.class: %.java
	\$(JAVAC) \$(JAVAC_FLAGS) \$<

%.jar: \$(JAVA_OBJS) \$(RESOURCE_OBJS)
	\$(FIND) \$(TOPLEVEL) \$(JAR_OBJS) -print | \$(XARGS) \\
	\$(JAR) \$(JAR_FLAGS) \$(JAR_FILE) 

%.u: %.java
	\$(JAVAC) \$(JIKES_DEP_FLAG) \$<


# -------
# Targets
# -------

.PHONY: all jar install uninstall doc clean depend tags

all::	\$(JAVA_OBJS)


help:
	\@echo "Usage: make {all|jar|install|uninstall|doc|clean|depend|tags}"


# Jar target
ifneq (\$(strip \$(JAR_FILE)),)
jar:  \$(JAR_FILE)
ifneq (\$(strip \$(JAR_DIR)),)
install:: \$(JAR_FILE)
	\@echo "===> [Installing jar file, \$(JAR_FILE) in \$(JAR_DIR)] "
	\$(INSTALL_DIR) \$(JAR_DIR) \$(check-exit)
	\$(INSTALL_FILE) \$(JAR_FILE) \$(JAR_DIR) \$(check-exit)
uninstall::
	\@echo "===> [Removing jar file, \$(JAR_FILE) from \$(JAR_DIR)] "
	\$(RM) \$(JAR_DIR)/\$(JAR_FILE)  \$(check-exit)
else
install::
	\@echo "No jar install dir defined"
endif
clean::
	\$(RM) \$(JAR_FILE)
else
jar:
	\@echo "No jar file defined"
endif



# Install target for Classes and Resources 
ifneq (\$(strip \$(CLASS_DIR)),)
install:: \$(JAVA_OBJS)
	\@echo "===> [Installing classes in \$(CLASS_DIR)] "
	\$(INSTALL_DIR) \$(CLASS_DIR) \$(check-exit)
	\$(foreach dir, \$(JAVA_DIRS) \$(RESOURCE_DIRS), \\
		\$(INSTALL_DIR) \$(CLASS_DIR)/\$(dir) \$(check-exit))
	\$(foreach file, \$(INSTALL_OBJS), \\
		\$(INSTALL_FILE) \$(file) \$(CLASS_DIR)/\$(file) \\
	\$(check-exit))
uninstall::
	\@echo "===> [Removing class-files from \$(CLASS_DIR)] "
	\$(foreach file, \$(INSTALL_OBJS), \\
		\$(RM) \$(CLASS_DIR)/\$(file) \\
	\$(check-exit))
else
# Print a warning here if you like. (No class install dir defined)
endif



# Depend target
ifeq (\$(findstring jikes,\$(JAVAC)),jikes)
depend: \$(XJAVA_OBJS) \$(DEPEND_OBJS)
	( \$(CAT) \$(DEPEND_OBJS) |  \$(SED) -e '/\\.class\$\$/d' \\
	  -e '/.*\$\$.*/d' > \$(MAKEFILE_DEPEND); \$(RM) \$(DEPEND_OBJS); )
else
depend:
	\@echo "mmake needs the jikes compiler to build class dependencies"
endif



# Doc target
ifneq (\$(strip \$(PACKAGES)),)
doc:	\$(JAVA_SRC)
	\@echo "===> [Installing java documentation in \$(DOC_DIR)] "
	\$(INSTALL_DIR) \$(DOC_DIR) \$(check-exit)
	\$(JAVADOC) -d \$(DOC_DIR) \$(JAVADOC_FLAGS) \$(PACKAGES)
else
doc:
	\@echo "You must put your source files in a package to run make doc"
endif



# Script target
ifneq (\$(strip  \$(SCRIPT_OBJS)),)
all::	 \$(SCRIPT_OBJS)
ifneq (\$(strip \$(SCRIPT_DIR)),)
install:: \$(SCRIPT_OBJS)
	\@echo "===> [Installing shell-scripts in \$(SCRIPT_DIR)] "
	\$(INSTALL_DIR) \$(SCRIPT_DIR) \$(check-exit)
	\$(foreach file, \$(SCRIPT_OBJS), \\
		\$(INSTALL_PROG) \$(file) \$(SCRIPT_DIR) \$(check-exit))
uninstall:: 
	\@echo "===> [Removing shell-scripts from \$(SCRIPT_DIR)] "
	\$(foreach file, \$(SCRIPT_OBJS), \\
		\$(RM) \$(SCRIPT_DIR)/\$(file) \$(check-exit))
else
# Print a warning here if you like. (No script install dir defined)
endif
clean::
	rm -f \$(SCRIPT_OBJS)
endif



# Tag target
tags:	
	\@echo "Tagging"
	\$(ETAGS) \$(filter-out \$(XJAVA_OBJS), \$(JAVA_SRC)) \$(XJAVA_SRC)



# Various cleanup routines
clean::
	\$(FIND) . \\( -name '*~' -o -name '*.class' \\) -print | \\
	\$(XARGS) \$(RM) 
	\$(FIND) . -name '*.u' -print | \$(XARGS) \$(RM)

ifneq (\$(strip \$(XJAVA_SRC)),)
clean::
	\$(RM) \$(XJAVA_OBJS)
endif

# ----------------------------------------
# Include the dependency graph if it exist
# ----------------------------------------
MAKEFILE_DEPEND	= makefile.dep
DEPEND	= \$(wildcard \$(MAKEFILE_DEPEND))
ifneq (\$(DEPEND),)
	include \$(MAKEFILE_DEPEND)
endif

EOT

close(MAKEFILE);

print "'$M' created\n";

exit(0);


# ------------------------------------------------------------------------- #
# ----------------------------- Subroutines ------------------------------- #
# ------------------------------------------------------------------------- #

sub getline($$)
{
  my $key= shift;
  my $dir= shift;
  my $j;
  print "$key [$dir]: ";
  chomp($j= <STDIN>);

  return ($j =~ /\w/ ? $j : $dir);
}


sub getdirline($$)
{
  my $key= shift;
  my $dir= shift;
  my $j;
  print "$key [$dir]: ";
  chomp($j= <STDIN>);
  if ( $j =~ /(\w)|(^\.)/ ) {
    $j .="/" if ( substr($j, -1) ne "/" );
    return $j;
  }

  return $dir;
}


sub getpreviewline($$$)
{
  my $key=     shift;
  my $preview= shift;
  my $dir=     shift;
  my $j;
  print "$key [$preview]: ";
  chomp($j= <STDIN>);

  return ($j =~ /\w/ ? $j : $dir);
}


sub do_get()
{
  my $cmd;

  # Get directories with java files
  $cmd= q$find . \( -name "*.java" -o -name "*.xjava" \) $
       .q$-print 2>/dev/null |$;
  @packages= do_find("p", $cmd);
  return unless @packages or $toplevel;

  # Get directories with shell-script files
  $cmd= q$find . \( -name "*.sh" \) -print 2>/dev/null |$;
  @scripts= do_find("s", $cmd);

  # Get directories with *only* resource files
  $cmd= q$find . \( -name "*.properties" -o -name "*.gif" -o -name $
       .q$"*.au" \) -print 2>/dev/null |$;
  # A hash with package values
  my %p= map { ($_), $_ } @packages;
  # Set only elements located in a package and not
  # already in the packages array
  @resources= grep { ! $p{$_} && $_ =~ /\./ } do_find("p", $cmd);
}


sub do_find($$)
{
  my $mode= shift; # p = use package syntax
  my $cmd= shift;
  my %unique= ();

  open(MYDIR, $cmd) or
    die "$PROG: Can't run the \"find(1)\" command: $!\n";

  # Get the directories
  while (<MYDIR>)
  {
    chomp;
    s,(.*)/.*,$1,;
    s,^./,,;
    s,/,.,g if ( $mode eq "p" );
    $unique{$_}= undef if $_ =~ /\w+/;
    $toplevel= "." if $_ =~ /\./;
  }
  close(MYDIR);

  return ( keys(%unique) ) ;
}

