Learning Makefiles as a Java developer. Part 2: phony targets, default goal and variables
edit 2023-12-24: add a link to part 3 of the series
In the first post we've learned a few of the basic concepts of Make. To do so, we've created a Java project containing a single Java file. I understand it does not represent the majority of "real world" Java projects.
Before we can move on to building "production-like" Java projects, I feel it is best if we introduce a few other Make concepts:
-
phony targets;
-
the default target;
-
automatic variables; and
-
variables.
They are usually used all over in a Makefile. So we should know them.
Let's continue.
Iteration 05: phony targets
To recap, this is how we have been invoking make to build the JAR file of our project:
$ make work/library.jar
Which is quite verbose.
Adding a jar
target
Let's improve on that. We would be easier if we could just say:
$ make jar
We can actually do that. Let's modify our Makefile and add the following target:
jar: work/library.jar
And let's remove all of the generated files:
$ rm -r work
And if we invoke Make:
$ make jar
javac -d work/main main/objectos/library/Say.java
jar --create --file=work/library.jar -C work/main .
It is an improvement.
Adding a clean
target
We could also use a clean
target as we have been doing the following quite a lot:
$ rm -rf work
Let's add it to our Makefile:
clean:
rm -rf work
And if we invoke Make:
$ make clean
rm -rf work
Nice. There's a catch though.
Targets are files (usually)
As we have seen in the previous post, targets are files.
So, in the current form, we are asking Make to build files named jar
and clean
.
To understand, let's:
-
invoke the
clean
target multiple times; -
create a file name
clean
; and -
invoke the
clean
target once again.
$ make clean
rm -rf work
$ make clean
rm -rf work
$ touch clean
$ make clean
make: 'clean' is up to date.
Once again, in the current form, we are asking Make to build a file named clean
.
But the rule we have declared does not create such file because that's not our intention.
Once we manually create the clean
file, make
does not execute the rule anymore because the file:
-
already exists; and
-
is up to date (as the rule has no prerequisites).
While creating the file named clean
is an unlikely event, we would like for the clean
target to always run.
Marking clean
and jar
as phony
To handle cases like the clean
target, Make has the concept of phony targets.
Let's alter our Makefile once again and mark both targets as phony. The full Makefile after the modification is listed below:
.PHONY: clean
clean:
rm -rf work
work/main/objectos/library/Say.class: main/objectos/library/Say.java
javac -d work/main main/objectos/library/Say.java
.PHONY: jar
jar: work/library.jar
work/library.jar: work/main/objectos/library/Say.class
jar --create --file=work/library.jar -C work/main .
Notice the two declarations of the .PHONY
special built-in target name
Now if we invoke clean
target once again:
$ make clean
rm -rf work
It works.
Even when the clean
file still exists.
Phony targets
So a phony target is:
one that is not really the name of a file; rather it is just a name for a recipe to be executed when you make an explicit request.
I should have mentioned earlier, but the GNU Make manual is a great (if not the best) resource for learning Make. The quote was taken from the manual.
Iteration 06: the default goal
In iteration #02 we invoked make
without any targets.
Let's see what happens if we try now:
$ make
rm -rf work
From the output, it seems make
executed the clean
target created in the previous section.
When a target is not specified in the command line, make
will execute the first target it finds in the Makefile.
This is called the default goal.
In general, Makefiles are written so that the default goal builds the entire project.
So let's turn the jar
target into our default goal.
Our Makefile becomes:
.PHONY: jar
jar: work/library.jar
.PHONY: clean
clean:
rm -rf work
work/main/objectos/library/Say.class: main/objectos/library/Say.java
javac -d work/main main/objectos/library/Say.java
work/library.jar: work/main/objectos/library/Say.class
jar --create --file=work/library.jar -C work/main .
We moved the jar
phony target to the top of our Makefile.
And let's invoke make
:
$ make
javac -d work/main main/objectos/library/Say.java
jar --create --file=work/library.jar -C work/main .
It built the JAR file.
Iteration 07: automatic variables
In recipes, it is usual to refer to:
-
the target path name;
-
the prerequisite path name; or
-
both
Make allows you to refer to them via automatic variables.
Target of the rule
The $@
automatic variable contains the value of the file name of the target of the rule.
So we can convert the target of our JAR file:
work/library.jar: work/main/objectos/library/Say.class
jar --create --file=work/library.jar -C work/main .
To the following:
work/library.jar: work/main/objectos/library/Say.class
jar --create --file=$@ -C work/main .
Notice the $@
variable in the --file
option of the jar
command.
First prerequisite
The $<
automatic variable contains the value of the name of the first prerequisite.
So our compilation target:
work/main/objectos/library/Say.class: main/objectos/library/Say.java
javac -d work/main main/objectos/library/Say.java
Becomes:
work/main/objectos/library/Say.class: main/objectos/library/Say.java
javac -d work/main $<
Notice the $<
variable as the file argument of the javac
command.
Automatic variables in action
So our Makefile is now:
.PHONY: jar
jar: work/library.jar
.PHONY: clean
clean:
rm -rf work
work/main/objectos/library/Say.class: main/objectos/library/Say.java
javac -d work/main $<
work/library.jar: work/main/objectos/library/Say.class
jar --create --file=$@ -C work/main .
Let's test it:
$ make clean
rm -rf work
$ make
javac -d work/main main/objectos/library/Say.java
jar --create --file=work/library.jar -C work/main .
It works as expected.
Iteration 08: variables
Automatic variables are a special kind of variables;
meaning make
also has the "regular" kind of variable.
Let's see variables, the regular ones, in action. We will use them to organize our Makefile.
Organizing the clean section
Let's start with the clean
section of our Makefile:
#
# clean
#
## work directory (all generated files go here)
WORK = work
.PHONY: clean
clean:
rm -rf $(WORK)
So, in a Makefile you declare and assign a value using the following:
WORK = work
The left hand side contains the name of the variable. The right hand side is the value assigned to the variable.
To use the variable reference you enclose the name with $(..)
like so:
clean:
rm -rf $(WORK)
With the WORK
variable declared, it will be easier if someone prefers to use a Maven style directory structure.
You just change the variable value to:
WORK = target
Additionally, we have also added a number of comments to our Makefile.
Comments are all lines that start with a '#
' character.
Organizing the compile section
Next, let's organize the compilation section of our Makefile:
#
# compile
#
## directory for the (main) compiled class files
CLASS_OUTPUT = $(WORK)/main
## javac command options
JAVACX = javac
JAVACX += -d $(CLASS_OUTPUT)
JAVACX += $<
work/main/objectos/library/Say.class: main/objectos/library/Say.java
$(JAVACX)
Notice the following:
-
the
CLASS_OUTPUT
variable references another variable, namely theWORK
variable; -
the
+=
operator is used to append more text to the existingJAVACX
variable; and -
the
JAVACX
variable is used inside a recipe.
Organizing the package section
Finally, let's organize the JAR building (or packaging) section of our Makefile:
#
# package
#
## JAR file name
JAR_FILE = $(WORK)/library.jar
## JAR command
JARX = jar
JARX += --create
JARX += --file=$(JAR_FILE)
JARX += -C $(CLASS_OUTPUT)
JARX += .
.PHONY: jar
jar: $(JAR_FILE)
$(JAR_FILE): work/main/objectos/library/Say.class
$(JARX)
Notice the following:
-
as we have declared the
JAR_FILE
variable we are using it instead of the automatic variable$@
in the JAR command; -
we are using a variable reference, namely
$(JAR_FILE)
, as both the target and prerequisite of rules.
Putting it all together
The full version of our Makefile becomes:
#
# all: default goal
#
.PHONY: all
all: jar
#
# clean
#
## work directory (all generated files go here)
WORK = work
.PHONY: clean
clean:
rm -rf $(WORK)
#
# compile
#
## directory for the (main) compiled class files
CLASS_OUTPUT = $(WORK)/main
## javac command options
JAVACX = javac
JAVACX += -d $(CLASS_OUTPUT)
JAVACX += $<
work/main/objectos/library/Say.class: main/objectos/library/Say.java
$(JAVACX)
#
# package
#
## JAR file name
JAR_FILE = $(WORK)/library.jar
## JAR command
JARX = jar
JARX += --create
JARX += --file=$(JAR_FILE)
JARX += -C $(CLASS_OUTPUT)
JARX += .
.PHONY: jar
jar: $(JAR_FILE)
$(JAR_FILE): work/main/objectos/library/Say.class
$(JARX)
We have added the all
target to be our default goal.
This was required because we moved the jar section to the top of our Makefile.
We could have also used the .DEFAULT_GOAL
special variable.
Additionally, it is no accident that the sections names are the same as names of phases of the Maven build lifecycle.
Let's make sure our build is working properly:
$ make clean
rm -rf work
$ make
javac -d work/main main/objectos/library/Say.java
jar --create --file=work/library.jar -C work/main .
It works!
Conclusion
In this blog post we have expanded on a few more basic concepts of Make.
Some may seem like nice to have features. They are more than that though. In particular:
-
phony targets may be useful when recursive invocations of make are required;
-
automatic variables are required when using implicit rules; and
-
automatic variables are required when using pattern rules.
We will see pattern rules in a future post of this series.
You can find the source code of the examples in this GitHub repository.
Continue reading
The third part of this series is already available.
You can continue reading by following this link.