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.
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:
- DEV_ROOT - Pointing to the development root directory as described above. Use "/" as directory separator also on MS-Windows.
- IS_UNIX - Should be set to true if the development platform is UNIX and left unset (default) if it is not. This variable is needed for the sole purpose of getting the file separator token used by the Java tools correct. (It is ; on MS-Windows and : on UNIX.)
- JAVA_HOME - For Java development. Points to the Java distribution that is being used, for instance /usr/local/jdk1.3. All Java tools used by the make system is referred to through this pointer, thus changing to a different Java version is as simple as resetting this variable. Use "/" as directory separator also on MS-Windows.
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 - One for each package, located within the package directory, and containing the list of source files that constitute the package. These files are very simple, and can potentially be auto created.
- Project Makefile - Located at $DEV_ROOT, listing all packages constitute the project, the executable class as well as documentation details.
- Main Makefile - Located in the $DEV_ROOT/make directory and containing all the fine details about how to produce targets from source. This is the heart and the brain of the make system.
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 © 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.