Learning Makefiles as a Java developer. Part 3: empty targets and more automatic variables
edit 2023-12-31: add a link to part 4 of the series
In the previous post of this series we discussed the concepts of phony targets, the default goal and Make variables. While we managed to do so with our current project the fact remains that it currently contains a single Java file. And a single Java file project is not representative of a project one usually finds in a professional setting.
In the third part of our blog series, we will begin to address this issue. In the process of doing that, we should discuss some additional Make features:
-
empty targets
-
the
$^
automatic variable
Let's continue.
Iteration 09: working with many classes in a single package
Quick reminder: our project is of no practical use; it is just a substrate for our Makefile learning.
Splitting the Say
class
Let's add more Java files to our project. For our current discussion, it is best if the new Java classes:
-
belong to the same package; and
-
have some degree of relationship between themselves.
So let's split the Say
class like the following:
package objectos.library;
public class Say {
private Hello hello;
private Hi hi;
Say(Hello hello, Hi hi) {
this.hello = hello;
this.hi = hi;
}
public void hello(String who) {
hello.to(who);
}
public void hi(String who) {
hi.to(who);
}
}
And here's our new Hello
class:
package objectos.library;
final class Hello {
public void to(String who) {
System.out.println("Hello " + who + "!");
}
}
The code for the Hi
class is very similar so we won't list it here.
Building our project
Let's remove all of the previously generated files:
$ make clean
rm -r work
The contents of our directory should be:
$ find -type f
./main/objectos/library/Hello.java
./main/objectos/library/Hi.java
./main/objectos/library/Say.java
./Makefile
Let's try to build our project:
$ make
javac -d work/main main/objectos/library/Say.java
main/objectos/library/Say.java:19: error: cannot find symbol
private final Hello hello;
^
symbol: class Hello
location: class Say
main/objectos/library/Say.java:20: error: cannot find symbol
private final Hi hi;
^
symbol: class Hi
location: class Say
main/objectos/library/Say.java:22: error: cannot find symbol
Say(Hello hello, Hi hi) {
^
symbol: class Hello
location: class Say
main/objectos/library/Say.java:22: error: cannot find symbol
Say(Hello hello, Hi hi) {
^
symbol: class Hi
location: class Say
4 errors
make: *** [Makefile:32: work/main/objectos/library/Say.class] Error 1
This is expected.
Our Makefile only knows about the Say.java
source file.
Setting the compiler source path
The build is failing because, when trying to compile the Say
class, javac
cannot find either the:
-
source files; or
-
class files
For the Hello
and Hi
classes.
Let's inform javac
where it should look for source files.
We will update our JAVACX
variable in the "compile" section of our Makefile:
## main source directory
MAIN = main
## javac command options
JAVACX = javac
JAVACX += -d $(CLASS_OUTPUT)
JAVACX += -g
JAVACX += --source-path $(MAIN)
JAVACX += $<
The changes are:
-
we have introduced a
MAIN
variable pointing to the main source directory; -
added the
--source-path
option wherejavac
should look for input source files; and -
added the
-g
flag so thatjavac
generates the debugging info.
Next, let's test our updated Makefile.
Testing our build
Let's invoke make
:
$ make
javac -d work/main --source-path main main/objectos/library/Say.java
jar --create --file=work/library.jar -C work/main .
Notice that we have compiled a single Java file instead of three.
Let's verify what has been generated:
$ find -type f
./main/objectos/library/Hello.java
./main/objectos/library/Hi.java
./main/objectos/library/Say.java
./Makefile
./work/main/objectos/library/Say.class
./work/main/objectos/library/Hello.class
./work/main/objectos/library/Hi.class
./work/library.jar
So javac
implicitly compiled both the Hello
and Hi
classes.
Let's verify the contents of our JAR file:
$ jar --list --file=work/library.jar
META-INF/
META-INF/MANIFEST.MF
objectos/
objectos/library/
objectos/library/Hello.class
objectos/library/Hi.class
objectos/library/Say.class
OK, we have managed to compile all of our classes.
Iteration 10: to be or not to be incremental
There's a problem with our current Makefile.
Let's introduce a (fake) change to our Hello
class:
$ touch main/objectos/library/Hello.java
And let's run our build:
$ make
make: Nothing to be done for 'all'.
As the Hello
source file has changed, it should have recompiled the Hello
class.
And, to be on the safe side, it should have also recompiled the Say
class;
the method of the Hello
class referenced by Say
might have a new signature or it might have been removed.
Recompile everything on source file changes
So, if there is a change in any of the source files, it is safer to recompile the whole project again.
This is the default and recommend behavior of the maven-compiler-plugin.
And a discussion on this topic can be found here.
Updating our Makefile
So, as we will recompile the whole project, we don't need to track individual generated class files; it is sufficient if we track only the individual source files.
In other words, we don't need individual targets for individual generated class files. Instead we should have a single target for the whole compilation step.
With this in mind, let's update the "compile" section of our Makefile.
The SOURCES
variable
First, we gather all of our source files in a single location:
## all of our source files
SOURCES = $(MAIN)/objectos/library/Hello.java
SOURCES += $(MAIN)/objectos/library/Hi.java
SOURCES += $(MAIN)/objectos/library/Say.java
The SOURCES
variable contains a whitespace separated list of our source files.
The compilation rule
Next, let's create a rule to compile our source code:
## compilation marker
COMPILE_MARKER = $(WORK)/compile-marker
$(COMPILE_MARKER): $(SOURCES)
$(JAVACX)
@touch $@
The target of our rule is an empty file. It serves as a marker as to when the last compilation has occurred.
The list of all of the source files serves as our prerequisite. In other words, compilation should be executed whenever any of the source file changes.
When this is the case, we invoke our javac
command.
After the compilation executes we create our marker file.
Remember that the $@
automatic variable contains the filename of the target.
The $^
automatic variable
We must also update our javac
command invocation:
## javac command options
JAVACX = javac
JAVACX += -d $(CLASS_OUTPUT)
JAVACX += -g
JAVACX += --source-path $(MAIN)
JAVACX += $^
The last argument is now the $^
automatic variable;
it contains the names of all of the prerequisites.
Updating the JAR file prerequisite
As a final step, we must update the rule for packaging our JAR file:
$(JAR_FILE): $(COMPILE_MARKER)
$(JARX)
The prerequisite was changed:
-
from the
Say.class
file; -
to the
$(COMPILE_MARKER)
file instead.
Testing our changes
Here's our updated "compile" section:
#
# compile
#
## main source directory
MAIN = main
## all of our source files
SOURCES = $(MAIN)/objectos/library/Hello.java
SOURCES += $(MAIN)/objectos/library/Hi.java
SOURCES += $(MAIN)/objectos/library/Say.java
## directory for the (main) compiled class files
CLASS_OUTPUT = $(WORK)/main
## compilation marker
COMPILE_MARKER = $(WORK)/compile-marker
## javac command options
JAVACX = javac
JAVACX += -d $(CLASS_OUTPUT)
JAVACX += -g
JAVACX += --source-path $(MAIN)
JAVACX += $^
$(COMPILE_MARKER): $(SOURCES)
$(JAVACX)
@touch $@
Let's test our build:
$ make clean
rm -rf work
$ make
javac -d work/main -g --source-path main \
main/objectos/library/Hello.java \
main/objectos/library/Hi.java \
main/objectos/library/Say.java
jar --create --file=work/library.jar -C work/main .
$ make
make: Nothing to be done for 'all'.
$ touch main/objectos/library/Hello.java
$ make
javac -d work/main -g --source-path main \
main/objectos/library/Hello.java \
main/objectos/library/Hi.java \
main/objectos/library/Say.java
jar --create --file=work/library.jar -C work/main .
$ make
make: Nothing to be done for 'all'.
The output has been edited slightly for presentation purposes.
It seems to be working correctly now. In particular, it recompiles the whole project when there's a change in a source file.
Let's check the generated files:
$ find -type f
./main/objectos/library/Hello.java
./main/objectos/library/Hi.java
./main/objectos/library/Say.java
./Makefile
./work/compile-marker
./work/main/objectos/library/Hello.class
./work/main/objectos/library/Hi.class
./work/main/objectos/library/Say.class
./work/library.jar
Notice that we have:
-
the compilation marker; and
-
the three class files.
Let's verify the contents of our JAR file:
$ jar --list --file=work/library.jar
META-INF/
META-INF/MANIFEST.MF
objectos/
objectos/library/
objectos/library/Hello.class
objectos/library/Hi.class
objectos/library/Say.class
It contains all of the class files.
Conclusion
Our project is of no practical use. Still we want it to be representative of a project one would find in a professional setting.
So, in this post, we have begun to add more classes to our project. All of the added classes belong to the same package initially.
In doing so we have discussed two more Make concepts:
-
empty targets
-
the
$^
automatic variable
You can find the source code of the examples in this GitHub repository.
Continue reading
The fourth part of this series is already available.
You can continue reading by following this link.