Search code examples
cmakefilegnu-make

How to write a Makefile with multiple object files, each having different dependencies, while minimizing code redundancy?


I've recently started learning about Makefiles, but I've encountered an issue involving multiple object files with different dependencies. Specifically, these are the dependencies:

  • main.o: main.c node.h funk2.h
  • funk1.o: funk1.c funk1.h
  • funk2.o: funk2.c funk2.h

As you can see main.o has 3 dependencies and both the funk files have 2 dependencies.

I wrote a MakeFile like this

all: start

start: main.o funk1.o funk2.o
    gcc $^ -g -Wall -o start

main.o: main.c node.h funk2.h
    gcc -c -g $<

%.o: %.c %.h
    gcc -c -g $<

clean:
    rm start *.o

I have a specific rule for main.o, and a generic rule for both funk1.o and funk2.o since they both depend on both .c and .h files. main.o has its own rule because it depends on one .c file and two .h files. My question is, is there any way to simplify this Makefile and use one rule for all the object files?

After some research I wrote this Makefile

all: start

start: main.o funk1.o funk2.o
    gcc $^ -g -Wall -o start

main.o: main.c node.h funk2.h
funk1.o: funk1.c funk1.h
funk2.o: funk2.c funk2.h

%.o: %.c
    gcc -c -g $<

clean:
    rm start *.o

This seems to work the same as the previous MakeFile but I'm very unsure of whats actually happening here. If this is a good approach to do what my question is, I would appreciate if someone could run me through what happens in this MakeFile when I invoke make.

What I think is happening: When we invoke Make we start at the all target which has "start" as its dependency. We then go to the target start and look at its dependencies. First we look at main.o and see that its a target so we check that target, main.o: main.c node.h funk2.h. We then check these dependencies and since they aren't targets we check their timestamps and determine that main.o is outdated. Now here is where I get stuck, what do we do next? My assumption is that since this target has no recipe it will look for another target that matches which is %.o: %.c. Which becomes main.o: main.c. But do we check if its outdated again here or do we skip this step since we have already determined that it's outdated? After this we repeat with the other object files and finally link everything together. Have I understood this correctly?


Solution

  • Note: in what follows, I use the word "build" in a somewhat idiosynchratic sense that you should understand as being full defined within.

    What I think is happening: When we invoke Make we start at the all target

    Yes, if you do not specify a different goal target(s) on the make command line, then make chooses as its goal the first explicit target in the makefile whose name does not begin with a .. In your case, that is target all, but that name is merely conventional, not inherently significant to make.

    which has "start" as its dependency. We then go to the target start

    Sort of. We do not "go to" start in the sense of transferring control, and especially not to the prerequisite-only rule that has start as an explicit target. Makefiles are not scripts. Rules are not functions. make's is a declarative language that embeds shell commands as data.

    all, being the goal target, is selected for building. To build it, make first builds each of its prerequisites, which in this case is just start. This reveals whether any of the prereqs were initially out of date and if so, brings them up to date. Afterward, if any were updated or are newer than all then all is updated by running its recipe. (This will always be the case for your all as long as no such file exists.) Running the recipe is purely notional for all because it has none.

    and look at [start's] dependencies.

    start is built via precisely the same procedure as for all.

    First we look at main.o

    main.o is not necessarily first, but yes, we build main.o and the other prereqs of start, again by the same procedure, because this is part of building start.

    and see that its a target

    Not exactly. What make sees is that it has a rule -- the pattern rule -- that is applicable for building main.o. That file does not necessarily need to be named as an explicit target of any rule for that to be true. Nor even to match the target pattern of an implicit rule present in the makefile, because make has a library of built-in rules that handle a lot of simple cases.

    so we check that target, main.o: main.c node.h funk2.h. We then check these dependencies

    Sort of. The explicit rule for main.o does designate prerequisites for it, which make will take into account. But the target is not the same thing as the rule, and that prerequisite-only rule is not the one that will be selected for building main.o.

    But yes, again following the same procedure, we build each of main.o's prerequisites.

    and since they aren't targets

    Not exactly. Again, the question is whether we have rules for building these, which is not the same as whether they are targets. They are targets because, as prerequisites of a rule, make may need to build them.

    There not being any rules applicable for building them, they are out of date only if they do not exist. And if there is no rule at all for a particular target, it does not exist, and it needs to be built, then the build fails.

    we check their timestamps and determine that main.o is outdated.

    Yes, as already described above, a target is out of date if it does not exist, if any of its prereqs were initially out of date, or if any of its prereqs are newer than it.

    Now here is where I get stuck, what do we do next? My assumption is that since this target has no recipe

    Targets do not have recipes. Rules (often) have recipes. An explicit rule without a recipe merely designates prerequisites for its target(s).

    You went off track earlier, and it is just here that you ground to a halt. You are imagining that the prerequisite-only rule for start is being executed like a function. But make rules are not functions.

    it will look for another target that matches which is %.o: %.c. Which becomes main.o: main.c.

    Sort of. make will have already selected the pattern rule for building main.o, and collected all its prerequisites from that rule and any prerequisite-only rules targeting it.

    But do we check if its outdated again here or do we skip this step since we have already determined that it's outdated?

    There is no "again" at this point. There is no control flow from the prerequisite-only rule to this one.

    make processes the whole rule set, including its built-in rules, before building anything. Having chosen or been given one or more goal targets, it uses the rule set to determine all the targets involved and the recipes, if any, to be used to build them, and to construct a dependency tree. The dependency tree constrains the order in which targets are selected for building, and influences which targets are initially considered out of date.

    After this we repeat with the other object files and finally link everything together.

    Effectively yes.

    Have I understood this correctly?

    You have pretty much understood the order of events correctly. You have not described an accurate model of how make arrives at that order.