Learning Makefiles as a Java developer. Part 3: empty targets and more automatic variables

Marcio Endo
December 24, 2023

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:

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:

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:

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:

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:

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:

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:

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.