Java Makefile
A simple replacement for Ant

Version 4.4, January 2011
Geotechnical Software Services
Copyright © 1999 - 2011

This document is available at http://geosoft.no/development/javamake.html

Abstract: A cross-platform (MS-Windows, Linux, UNIX) makefile for a wide range of programming languages and tools (Java, C, C++, Fortran, Rmi, Javadoc, Makedepend etc.). Suitable for single file projects and scalable to multi million file projects. The Makefile is written once and for all; no further editing required. The file is well-organized and suitable as an example for learning GnuMake.

Table of Content

    Introduction
    Organizing the Development
    Prerequisites
    The Makefiles
    Running Make
    A Package Makefile Generator

Introduction

The last few years integrated development environments (IDE) have become popular for software development. IDE's provides the developer with all the tools required for the development task such as file manager, version control system, modeling tools, editor, compiler, debugger, user interface builder, execution environment, profiler and performance analyzing tools. In particular the user interface builder part of IDE's has proved useful, and is the backbone of IDE's like VisualBasic, Delphi, VisualC++, VisualCafé to name a few.

In many cases however, it is far more efficient to do code development in a pure terminal oriented environment. This is done to avoid the vast overhead of the IDE itself, to achieve better control of the development process and to be able to choose development tools like editors and debuggers on an individual basis. For these reasons it is not uncommon to divide projects into UI modules and non-UI modules, and use IDE's only on the former.

Doing development in a pure environment requires a powerful make setup to be efficient. The makefile setup provided here is powerful yet simple. It was created to support large scale multi-platform development, but is equally well suited for the single source file project. Currently it supports Java, C, C++, Fortran but can easily be extended to any language or domain.

The makefiles in this document uses GnuMake syntax. GnuMake is the default make system on the Linux platform, it is available on all UNIX platforms, and a Microsoft Windows version can be downloaded from here. (Look for the file UnxUtils.zip. Download and unpack it, and make sure the wbin/ directory becomes part of your path.) The actual make command in GnuMake is sometimes called gnumake and sometime just make (as is the case on Linux). In this document the latter is used consistently.


Organizing the Development

Some directory is chosen to be the development root directory (denoted DEV_ROOT below), and underneath it should the following subdirectories be created:

  $DEV_ROOT/src
           /obj
           /lib
           /docs
           /make
	   /bin

The src/ directory holds source files (.java, .c++, .gif etc.) organized in units called packages. The directory structure should follow the Java convention of reversed domain name which ensures universally unique file identifiers. This is useful also for C/C++/Fortran based projects. For instance:

  $DEV_ROOT/src/com/adobe/...

The content of one directory constitutes one package. A typical package setup might be:

  $DEV_ROOT/src/com/adobe/illustrator/ui/window/
                         /illustrator/ui/dialogs/
                         /illustrator/ui/print/
                         /illustrator/ui/print/postscipt/
                         /illustrator/ui/print/pdf/
                         /acroread/editor/.../...
                         /acroread/editor/.../...
                         /...

For Java source, the package statement must always reflect the the name indicated by the directory of each source file.

The obj/ directory contains target files produced by the source files, and the directory structure will be identical to the one int the src tree. The sub structure will be automatically produced by make, but the obj directory itself must be created in advance.

The lib/ directory contains all libraries in use. This includes 3rd-party libraries (copied here manually), as well as libraries produced by make. For the latter, there will be one library per package (i.e. directory) within the source tree. Java source produce .jar libraries while C/C++/Fortran source produce .so libraries. The library is named according to the package location, so Java code in $DEV_ROOT/src/com/adobe/utils are put in $DEV_ROOT/lib/comadobeutil.jar and C code in $DEV_ROOT/src/no/geosoft/math/calculus are put in $DEV_ROOT/lib/nogeosoftmathcalculus.so and so on.

The docs/ directory will hold all output from automatic documentation tools like Javadoc or Doxygen. The entry point for the documentation depends on the tool, but for Javadoc it is the file $DEV_ROOT/docs/index.html.

The make/ directory will hold the main Makefile (shown below) and a script for producing package makefiles. It can optionally be used for holding make logs etc.

The bin/ directory is where make will put non-Java executables.


Prerequisites

The following three environment variables has to be set prior to the use of the make system given below:


The Makefiles

There are three different makefiles involved. The first two defines the project at hand and will differ for each project, while the third is generic and can be used as is:


Package Makefile

The package makefiles should be called Makefile and should be located within the src tree, one per package. An example package makefile is shown below:

  Source = \
          ByteSwapper.java \
          Ebcdic.java \
          Formatter.java \
          IntMap.java \
          javacup.gif \
          properties.txt \

  RmiSource =

  Main      = ByteSwapper

  include $(DEV_ROOT)/Makefile

The Source entry lists all the source files. .java files will be passed to the Java compiler, .c files to the C-compiler and so on. Other files (like the .gif and .txt above) will be copied unprocessed to the obj tree.

The RmiSource entry lists all Java source files that are to be processed by the rmi compiler. Note that these files must also be listed under the Source entry as they are also processed by javac.

The Main entry is optional and indicates which class contains the main() method. Leave open if none of them do. For a Java program there is only one main entry point, but it is common to include main() in other classes in order to test that specific class. The setup above makes it possible to run these test applications by issuing "make run" from the package level. See below.

Project Makefile

The project makefile should be called Makefile and should be located at $DEV_ROOT. The project makefile lists all packages and jar archives that constitutes the project.

An example makefile for a typical Java project is shown below:

  JavaPackages = \
          no/geosoft/directory \
	  no/geosoft/user \
	  no/geosoft/database \

  JavaLibraries = \
  	  mysql-connector.jar \

  JavaMainClass = \
          no.geosoft.database.Main

  RunParameters =
  
  # Javadoc
  JavadocWindowTitle = 'Geotechnical Software Services - API Specification'
  JavadocDocTitle    = 'GeoSoft API'
  JavadocHeader      = 'GeoSoft API'
  JavadocFooter      = '<font size="-1">Copyright &copy; 2004 - Geotechnical Software Services <a href="http://geosoft.no">geosoft.no<a></font>'

  include $(DEV_ROOT)/make/Makefile

A similar example for a typical C++ project:
  Packages = \
          no/geosoft/directory \
	  no/geosoft/user \
	  no/geosoft/util \

  IncludeDirs = \
	  /usr/include \
          /usr/include/g++-2 \
          /usr/X11R6/include \

  LibraryDirs = \
          /usr/lib \
          /usr/X11R6/lib \

  Libraries = \
          mysql \


  include $(DEV_ROOT)/make/Makefile

The JavaPackages entry lists all Java packages governed by this makefile. The Packages entry list all other packages.

The IncludeDirs list include directories referenced in C/C++/Fortran code. LibraryDirs and Libraries identifies .so libraries for C/C++/Fortran based linking, while JavaLibraries lists 3-rd party jars located in the $DEV_ROOT/lib directory.

The JavaMainClass and RunParameters are used for executing Java applications.

The Javadoc entries are optional and used for decoration of the produced Javadoc documentation.

Main Makefile

The Main makefile is the heart and brain of the make setup, and should be located in the $DEV_ROOT/make directory and be called Makefile. It is not executed directly, but rather included by the project makefile. This makefile contains everything needed to build the project described in the project makefile and it is rather complex. However, it it is written once and for all, and can to a large extent be left alone as is. Remember; This is the only Makefile you will ever need.

The Main Makefile is shown below. Copy-paste it from the browser and use it for your own projects:


  #---------------------------------------------------------------------------
  #  (C) 1999 - 2011 GeoSoft - Geotechnical Software Services
  #  info@geosoft.no - http://geosoft.no
  #
  #  This program is free software; you can redistribute it and/or
  #  modify it under the terms of the GNU General Public License
  #  as published by the Free Software Foundation; either version 2
  #  of the License, or (at your option) any later version.
  #
  #  This program is distributed in the hope that it will be useful,
  #  but WITHOUT ANY WARRANTY; without even the implied warranty of
  #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  #  GNU General Public License for more details.
  #
  #  You should have received a copy of the GNU General Public License
  #  along with this program; if not, write to the Free Software
  #  Foundation, Inc., 59 Temple Place - Suite 330, Boston,
  #  MA  02111-1307, USA.
  #---------------------------------------------------------------------------
  #---------------------------------------------------------------------------
  #
  #  GnuMake crash course:
  #
  #  target : depends
  #    rule
  #
  #  target  - the parameter given to make. I.e. what to build
  #  depends - file or other targets target depends on
  #  rule    - how to create target (note that rule is preceeded by a TAB char)
  #  $(VAR)  - environment variable or variable defined above
  #  $@      - Current target
  #  $*      - Current target without extension
  #  $<      - Current dependency
  #
  #---------------------------------------------------------------------------
  #---------------------------------------------------------------------------
  #
  #  Directories
  #
  #---------------------------------------------------------------------------
  
  SourceDir        = $(DEV_ROOT)/src
  TargetDir        = $(DEV_ROOT)/obj
  LibDir           = $(DEV_ROOT)/lib
  MakeDir          = $(DEV_ROOT)/make
  BinDir           = $(DEV_ROOT)/bin
  DocsDir          = $(DEV_ROOT)/docs
  CurrentDir       = $(CURDIR)

  ifdef Source
    Package          = $(subst $(SourceDir)/,,$(CurrentDir))
    PackageList      = $(Package)
    PackageSourceDir = $(SourceDir)/$(Package)
    PackageTargetDir = $(TargetDir)/$(Package)
    JavaMainClass    = $(subst /,.,$(Package)).$(Main)
  else
    PackageList      = $(Packages) $(JavaPackages)
  endif

  PackageListLoop  = $(patsubst %,$(SourceDir)/%/.loop,$(PackageList))

  JRE              = $(JAVA_HOME)/jre/lib/rt.jar

  ifdef IS_UNIX
    X = :
  else
    X = \;
  endif

  
  #---------------------------------------------------------------------------
  #
  #  Classification of files
  #
  #---------------------------------------------------------------------------

  # Source
  JavaFiles              = $(filter %.java,  $(Source))
  CppFiles               = $(filter %.cc,    $(Source))
  CFiles                 = $(filter %.c,     $(Source))
  FortranFiles           = $(filter %.f,     $(Source))
  CorbaFiles             = $(filter %.idl,   $(Source))
  OtherSourceFiles       = $(filter-out $(JavaFiles) $(CppFiles) $(CFiles) \
  		                      $(FortranFiles) $(CorbaFiles), \
  	                              $(Source))
  ManifestFile           = $(PackageSourceDir)/Manifest
  SourceFiles            = $(JavaFiles:%.java=  $(PackageSourceDir)/%.java)\
                           $(CppFiles:%.cc=     $(PackageSourceDir)/%.cc)\
                           $(CFiles:%.c=        $(PackageSourceDir)/%.c)\
                           $(FortranFiles:%.f=  $(PackageSourceDir)/%.f)

  
  # Target
  JavaClassFiles         = $(JavaFiles:%.java=  $(PackageTargetDir)/%.class)
  JavaClassFilesRel      = $(JavaFiles:%.java=  $(Package)/%.class)
  RmiStubFiles           = $(RmiSource:%.java=  $(PackageTargetDir)/%_Stub.class)
  RmiSkeletonFiles       = $(RmiSource:%.java=  $(PackageTargetDir)/%_Skel.class)
  JniClassFiles          = $(JniSource:%.java=  $(PackageTargetDir)/%.class)
  JniHeaders             = $(JniSource:%.java=  $(PackageSourceDir)/%.h)
  ObjectFiles            = $(CFiles:%.c=        $(PackageTargetDir)/%.o)\
  		         $(CppFiles:%.cc=     $(PackageTargetDir)/%.o)\
  		         $(FortranFiles:%.f=  $(PackageTargetDir)/%.o)
  OtherTargetFiles       = $(OtherSourceFiles:%=$(PackageTargetDir)/%)

  ThirdPartyJarsTmp = $(patsubst %,$(LibDir)/%,$(JavaLibraries))
  ThirdPartyJars    = $(subst $(Space),$(X),$(ThirdPartyJarsTmp))

  ifneq "$(words $(JavaFiles))" "0"
    JavaPackageName   = $(subst /,.,$(Package))
    JarFile           = $(LibDir)/$(subst /,,$(Package)).jar
  endif
  ifneq  "$(words $(ObjectFiles))" "0"
    DependencyFile    = $(PackageSourceDir)/Makedepend
    SharedLibrary     = $(LibDir)/lib$(subst /,,$(Package)).so
    StaticLibrary     = $(LibDir)/lib$(subst /,,$(Package)).a
    ifneq "$(Main)" ""
      Executable        = $(BinDir)/$(Main)
    endif
  endif
  
  #
  # Misc
  #
  ClassPath        = $(JRE)$(X)$(TargetDir)$(X)$(ThirdPartyJars)
  JavaPackageNames = $(subst /,.,$(JavaPackages))
  IncludePath      = -I$(SourceDir) $(IncludeDirs:%=-I%)
  LibDirs          = -L$(LibDir)    $(LibraryDirs:%=-L%)
  LocalLibs        = $(subst /,,$(Packages))
  LibList          = $(LocalLibs:%=-l%) $(Libraries:%=-l%)


  
  #---------------------------------------------------------------------------
  #
  #  Tools & Options
  #
  #---------------------------------------------------------------------------
  Print                  = @echo
  Copy                   = cp
  CCompiler              = gcc
  CppCompiler            = gcc
  Linker                 = gcc
  MakeDepend             = makedepend
  MakeDir                = mkdir -p
  Delete                 = rm -fr
  StaticArchiver         = ar
  DynamicArchiver        = gcc
  FortranCompiler        = f77
  JavaCompiler           = $(JAVA_HOME)/bin/javac
  JavaArchiver           = $(JAVA_HOME)/bin/jar
  JarSigner              = $(JAVA_HOME)/bin/jarsigner
  JavadocGenerator       = $(JAVA_HOME)/bin/javadoc
  JniCompiler            = $(JAVA_HOME)/bin/javah
  RmiCompiler            = $(JAVA_HOME)/bin/rmic
  JavaExecute            = $(JAVA_HOME)/bin/java
  Purify                 = purify
  WordCount              = wc
  List                   = cat

  MakeOptions            = -k -s
  MakeDependOptions      =
  StaticArchiverOptions  = rc
  DynamicArchiverOptions = -shared
  JavaArchiverOptions    =
  JniOptions             =
  RmiOptions             = -d $(TargetDir) -classpath $(ClassPath) \
  			 -sourcepath $(SourceDir)
  FortranOptions         =
  JavaCompilerOptions    = -d $(TargetDir) -classpath $(ClassPath) \
  			 -sourcepath $(SourceDir) -deprecation
  JavaRunOptions         = -classpath $(ClassPath)
  PurifyOptions          =
  JavadocOptions         = -d $(DocsDir) \
  			 -sourcepath $(SourceDir) \
  			 -classpath $(ClassPath) \
  			 -author \
  			 -package \
  			 -use \
  			 -splitIndex \
  			 -version \
  			 -link file:$(JAVA_HOME)/docs/api \
  			 -windowtitle $(JavadocWindowTitle) \
  			 -doctitle $(JavadocDocTitle) \
  			 -header $(JavadocHeader) \
  			 -bottom $(JavadocFooter)
  WordCountOptions       = --lines

  Empty                  =
  Space                  = $(Empty) $(Empty)


  
  #---------------------------------------------------------------------------
  #
  # Rules
  #
  #---------------------------------------------------------------------------
  
  default : build

  %.loop :
  	@$(MAKE) $(MakeOptions) -C $(subst .loop,,$@) _$(MAKECMDGOALS)all

  # Create target directory
  $(PackageTargetDir) :
  	$(MakeDir) $@

  # .c -> .o
  $(PackageTargetDir)/%.o : $(PackageTargetDir) $(PackageSourceDir)/%.c
  	$(Print) $@
  	@$(CCompiler) $(COptions) -c $(IncludePath) $< -o $@

  %.o : $(PackageSourceDir)/%.c
  	$(MAKE) $(MakeOptions) $(PackageTargetDir)/$@

  # .cc -> .o
  $(PackageTargetDir)/%.o : $(PackageSourceDir)/%.cc
  	$(Print) $@
  	$(CppCompiler) $(CppOptions) -c $(IncludePath) $< -o $@

  %.o : $(PackageSourceDir)/%.cc
  	$(MAKE) $(MakeOptions) $(PackageTargetDir)/$@

  # .f -> .o
  $(PackageTargetDir)/%.o : $(PackageSourceDir)/%.f
  	$(Print) $@
  	@$(FortranCompiler) $(FortranOptions) -c $< -o $@

  %.o : $(PackageSourceDir)/%.f
  	$(MAKE) $(MakeOptions) $(PackageTargetDir)/$@

  # .java -> .class
  $(PackageTargetDir)/%.class : $(PackageSourceDir)/%.java
  	$(Print) $@
  	@$(JavaCompiler) $(JavaCompilerOptions) $<

  %.class : $(PackageSourceDir)/%.java
  	@$(MAKE) $(MakeOptions) $(PackageTargetDir)/$@

  # .class -> .h
  $(PackageSourceDir)/%.h : $(PackageTargetDir)/%.class
  	$(Print) $@
  	$(JniCompiler) $(JniOptions) $(JavaPackageName).$*

  %.h : %.class
  	$(MAKE) $(MakeOptions) $(PackageSourceDir)/$@

  # .o -> .a
  $(LibDir)/%.a : $(ObjectFiles)
  	$(Print) $@
  	@$(StaticArchiver) $(StaticArchiverOptions) $@ $(ObjectFiles)

  %.a : $(ObjectFiles)
  	$(MAKE) $(MakeOptions) $(LibDir)/$@

  # .o -> .so
  $(LibDir)/%.so : $(ObjectFiles)
  	$(Print) $@
  	$(DynamicArchiver) $(ObjectFiles) $(DynamicArchiverOptions) -o $@

  %.so : $(ObjectFiles)
  	$(MAKE) $(MakeOptions) $(LibDir)/$@

  # .class -> .jar
  $(LibDir)/%.jar : $(JavaClassFiles) $(OtherTargetFiles)
  	$(Print) $@
  	@cd $(TargetDir); $(JavaArchiver) -cf $@ \
  	$(JavaClassFilesRel) $(OtherTargetFiles)

  %.jar : $(JavaClassFiles) $(OtherTargetFiles)
  	$(MAKE) $(MakeOptions) $(LibDir)/$@

  # .class -> JavaDoc
  javadoc :
  	$(Print) $(JavaPackageNames) > $(DEV_ROOT)/packages.tmp
  	$(JavadocGenerator) $(JavadocOptions) @$(DEV_ROOT)/packages.tmp
  	$(Delete) $(DEV_ROOT)/packages.tmp
  	$(Print) Done JavaDoc.

  # .class -> _Stub.class 
  $(PackageTargetDir)/%_Stub.class : $(PackageTargetDir)/%.class
  	$(Print) $@
  	$(RmiCompiler) $(RmiOptions) $(JavaPackageName).$*

  %_Stub.class : %.class
  	$(MAKE) $(MakeOptions) $(PackageTargetDir)/$@

  # .class -> _Skel.class
  $(PackageTargetDir)/%_Skel.class : $(PackageTargetDir)/%.class
  	$(Print) $@
  	$(RmiCompiler) $(RmiOptions) $(JavaPackageName).$*

  %_Skel.class : %.class
  	$(MAKE) $(MakeOptions) $(PackageTargetDir)/$@

  # Executable
  $(Executable) : $(ObjectFiles)
  	$(Print) $@
  	$(Linker) $(LinkOptions) $(LibDirs) $(LibList) $(ObjectFiles) -o $@

  # Anything else is just copied from source to target
  $(PackageTargetDir)/% : $(PackageSourceDir)/%
  	$(Print) $@
  	$(Copy) $< $@

  # make (or make build)
  build : $(PackageListLoop)
  	$(Print) Done build.

  _all : _buildall

  _buildall : \
  	$(DependencyFile) \
  	$(PackageTargetDir) \
  	$(ObjectFiles) \
  	$(JavaClassFiles) \
  	$(RmiStubFiles) \
  	$(RmiSkeletonFiles) \
  	$(OtherTargetFiles) \
  	$(SharedLibrary) \
  	$(StaticLibrary) \
  	$(JarFile) \
  	$(Executable)


  # make clean
  clean : $(PackageListLoop)
  	$(Print) Done clean.

  _cleanall :
  	$(Delete) $(PackageTargetDir)/* \
  		  $(JarFile) \
  	          $(SharedLibrary) \
  	          $(StaticLibrary) \
  	          $(Executable) \
  	          $(DependencyFile)


  # make depend
  depend : $(PackageListLoop)
  	$(Print) Done dependencies.

  _dependall : $(DependencyFile)

  $(DependencyFile) :
  	$(Print) $@
  	@cd $(PackageSourceDir); \
  	$(MakeDepend) $(MakeDependOptions) -f- -p$(PackageTargetDir)/ \
  	$(IncludePath) $(Source) > $(DependencyFile)

  # make lib
  lib    : $(PackageListLoop)
  	$(Print) Libraries built.

  _liball : $(JarFile) $(SharedLibrary) $(StaticLibrary)

  jar : $(JarFile)

  jarsign : $(JarFile)
  	$(JarSigner) -keystore GeoSoftKeystore $(JarFile) myself

  # make statistics
  _statisticsall :
  	@$(Print) $(SourceFiles) >> $(DEV_ROOT)/files.tmp

  statistics : $(PackageListLoop)
  	@$(List) $(DEV_ROOT)/files.tmp | xargs $(WordCount) $(WordCountOptions)
  	@$(Delete) $(DEV_ROOT)/files.tmp
  	$(Print) Done statistics.

  # make pure
  $(Executable).pure :
  	$(Purify) $(PurifyOptions) $(CppCompiler) $(LinkOptions) $(LibDirs) \
  	$(LibList) $(ObjectFiles) -o $@

  pure : $(Executable).pure

  # Execute
  _runexe :
  	$(Executable) $(RunParameters)

  _runjava :
  	$(JavaExecute) $(JavaRunOptions) $(JavaMainClass) $(RunParameters)

  run : _runjava


  ifdef $(DependencyFile)
  -include $(DependencyFile)
  endif


Running Make

Using the make system is quite simple. You will run it from either the package level or from the project level ($DEV_ROOT).

The following commands can be applied from the package level (i.e when standing in a given package directory within the src tree):

   make - Process all source files in the package

   make clean - Remove all target files of the package

   make SomeTarget - Produce a specific file like Math.class or math.o. etc.

   make run - Execute the Java class identified by Main in the package Makefile.

The following commands can be applied from the project level (i.e while standing in the $DEV_ROOT directory):

   make - Process all source files in all packages

   make clean - Remove all produced files in all packages

   make lib - Create all libraries

   make javadoc - Create documentation for entire project

   make run - Run application

   make depend - Create dependency information (non-Java)

   make statistics - Produce LOC statistics for the entire project


A Package Makefile Generator

A package makefile is nothing but a list of the files in the package directory. It can easily be generated automatically and the following script do just that. The script should be run while standing in the package directory.

  #!/bin/csh -f
  
  #***************************************************************************
  #
  # Makefile generator script
  #
  # (C) 2011 GeoSoft - Geotechnical Software Services
  # info@geosoft.no - http://geosoft.no
  #
  # This program is free software; you can redistribute it and/or
  # modify it under the terms of the GNU General Public License
  # as published by the Free Software Foundation; either version 2
  # of the License, or (at your option) any later version.
  #
  # This program is distributed in the hope that it will be useful,
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  # GNU General Public License for more details.
  #
  # You should have received a copy of the GNU General Public License
  # along with this program; if not, write to the Free Software
  # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  #
  #***************************************************************************
  
  set file = Makefile

  rm -f $file
  touch $file

  printf "Source    = \\\n" >> $file

  foreach SourceFile (ls *.c *.cc *.java *.f *.gif *.jpg`)
      printf "\t%s \\\n" ${SourceFile} >> $file
  end
  printf "\n" >> $file

  printf "RmiSource =\n\n" >> $file
  printf "Main      =\n\n" >> $file
  printf "include %s" '$(DEV_ROOT)/Makefile >> $file

Note that the RmiSource and the Main tags cannot be set automatically and, if required, these should be added after the script has been run.