Darryl
Darryl

Reputation: 6247

Makefile order of dependencies

I was just handed a Makefile that has the following:

rpm: clean $(JARFILE)
    # commands here...

As you might have guessed, the clean target deletes the jar if it exists. Is this Makefile correct? Is there a guarantee that the clean target will be run before the JARFILE target? I'm not asking about style, but correctness. Is there a chance the JARFILE will be built but then just blown away by the clean target?

I'm using gnu make so that's what I'm most interested in; I'm not sure if this is intended to be portable to other flavors of make.

Upvotes: 2

Views: 330

Answers (2)

Carl
Carl

Reputation: 1029

Is there a guarantee that the clean target will be run before the JARFILE target?

make in general: no, there is no such guarantee.

GNU make:

  • if there's a reason to resolve prerequisites in some certain order, that order will be used
  • otherwise they will be resolved left-right [1]

For your specific rule, with the prereqs clean $(JARFILE), it isn't likely that the rest of the Makefile describes a dependency tree that gives GNU make a reason to resolve $(JARFILE) before clean.

It is possible in theory though. E.g. consider this Makefile:

# --- file: Makefile ---

rpm: clean A.jar
    @$(rpm_build)

A.jar: A.java
    cp $^ $@
    @printf "[done: '$@']\n\n"

clean: A.jar
    @# Error msg if attempting to delete non-existing file, so I
    @# I made sure it will exist before rm-ing it by adding A.jar
    @# as a prerequisite to `clean` above. //consultant
    rm A.jar
    @printf "[done: '$@']\n\n"


rpm_build = @[ -e $(JARFILE) ] && echo "rpm: OK." \
                               || echo "rpm: ERR: no '$(JARFILE)'"

Try to make the rpm target:

$ touch A.java
$ gmake

cp A.java A.jar
[done: 'A.jar']

rm A.jar
[done: 'clean']

rpm: ERR: no 'A.jar'

The dependency graph of Makefile is resolved by building A.jar, then doing clean, then building rpm. With this, all prereqs are honored - GNU make was not at fault for the company's build system breaking down due to the failing rpm target (someone else was).


A better option

It's a good idea to handle any restrictions on in what order prereqs are resolved through the dependency graph. This is clearer (not everyone knows about GNU make's intricacies), makes the Makefile (more) portable, and is more robust [2].

Below is an example. I added the intermediate target A_clean_jar so we still have the possibility of building A.jar without first triggering a clean.

# --- file: Makefile ---

rpm: A_clean_jar
    @$(inspect_jar)

A_clean_jar: A.java clean
    $(build_jar)
    @printf '[done: created pristine jar]\n\n'

A.jar: A.java
    $(build_jar)
    @printf '[done: created jar]\n\n'
    
clean:
    @rm -f A.jar
    @printf '[done: jar cleaned out of existence]\n\n'


build_jar = cp A.java A.jar

inspect_jar = @[ -e A.jar ] && printf "rpm: OK.\n\n" \
                            || printf "rpm: ERR: no 'A.jar'\n\n"

Let's try:

$ touch A.java
$ gmake rpm

[done: jar cleaned out of existence]

cp A.java A.jar
[done: created pristine jar]

rpm: OK.


$ # so: first `clean`, then build A.jar, then the rpm target - all well.

$ # now: build target A.jar - no preceding `clear` expected:

$ touch A.java
$ gmake A.jar

cp A.java A.jar
[done: created jar]

$ # check.

[2] What about the "robustness" benefits mentioned earlier?

A few months later, the CTO glanced over the repos after noticing a slight increase in the data center bills. A commit caught his eye, issued by some test engineer of a seemingly unrelated department, supposedly maintaining the intranet legacy PHP and sometimes helping out the secretary of the accounting assistant with Excel scripts. Recognizing the commit was made to the same file that was involved in the build system havoc some time ago, causing an expensive delay of an upcoming launch event, he had a look - and found this:

#clean
clean: A.jar
    @# Added back my `clean: A.jar` above - this time I tested it
    @# before pushing, and now nothing crashed. // your guy
    rm A.jar
    @# rm -f A.jar  (avoid -f with rm - dangerous)
    @printf "[done: '$@']\n\n"

Some moments later, a colleague of the CTO noticed this on his screen:

$ # let's just see how this behaves
$ gmake

java.c A.java && jar cvf A.jar A.class
[done: created jar]

[done: jar cleaned out of existence]

java.c A.java && jar cvf A.jar A.class
[done: created pristine jar]

rpm: OK.

$ less Makefile

$ git revert HEAD -m "reverting: prev. commit caused an unneccessary "\
"build (jar created twice), but nothing worse than that this time"

$ git log -2 --pretty=format:'%ae'
[email protected]

$ gh api -X DELETE /repos/admin/spacex-padctrl-prod/collaborators/[email protected]

$ >> ~/notesToSelf.txt echo 'avoid long-duration consultant contracts'

footnotes

[1] at least that's what's indicated by MadScientist's other answer (he is the maintainer of GNU make)

Upvotes: 2

MadScientist
MadScientist

Reputation: 101051

If you run make without any parallelism enabled (without -j) then your makefile will work properly. Make does guarantee that prerequisites are built in the order that they're listed in the makefile, if they are run serially.

However if you enable -j then both targets will probably be run in parallel and then you could run into trouble.

Upvotes: 2

Related Questions