Bit Of A Hack

Home Archives
logo

Keep up to date with Bit Of A Hack...

GNU Make 'percent' patterns are awkward [Permalink]

Wrote some make magic at work yesterday after much experimenting:

So, you have a tree that looks like this:

|--Makefile 
|--projects
| |--project1
| | |--project1.c
| |
| |--project2
| | |--project2.c

And you want your Makefile to generate this output after doing "make project1.o project2.o"

|--Makefile 
|--projects
| |--project1
| | |--project1.c
| |
| |--project2
| | |--project2.c
|
|--project1.o
|--project2.o
|

(If these were truly separate projects, you would probably have two makefiles. This is only an example and so the name project is probably a bad one.)

So, looks simple right? You are going to build with a gcc command like this:

gcc -c <path-to-.c> -o <path-to-.o>

And you want to build any .o file from its .c file in a folder with the same name as that c file. So you want your target to be anything matching this pattern:

%.o

(Anything that ends in ".o".) And you want the prerequisite to be the c file in the folder of the same name. With all that, you write this rule:

%.o: projects/%/%.c
    gcc -c $^ -o $@

But it won't work. Hmph! make will only expand up to one percent sign in your prerequisite pattern. ARGH! What do I do??

Well... perhaps pattern specific variables will help? These are variables that can change depending on what pattern matched the file being built.

For example, if files matching the pattern foo_%.o need to use a different compiler to files matching the pattern bar_%.o then you can do something like this;

foo_%.o: CC = gcc

bar_%.o: CC = some-other-gcc-like-thing

%.o: %.c
    $(CC) -c $^ -o $@

And this works! So perhaps you can employ a similar trick to do what you wanted before? Lets give it a go...

%.o: OBJECTNAME = %

%.o: projects/$(OBJECTNAME)/$(OBJECTNAME).c
    gcc -c $^ -o $@

Sounds like good theory. This will not work however. The pattern specific variable will not do what you want. It will be a literal percent sign as this is not expanded.

That's an easy fix though:

%.o: OBJECTNAME = $(basename $@)

$(basename VAR) removes the extension from VAR. $@ is, as usual, the target name. if your target is to be in a subdir, you may also need $(notdir VAR)

Another problem is that the variable is not set at the point where the prerequisites are expanded. I haven't looked into why this is but I presume the reason is because make follows this flow;

- invoke make with target project1.o

- parse Makefile

- found pattern specific variable definition for pattern that matches, ignore for now...

- found target pattern that matches

- expand prerequisites (pattern specific variable not yet set)

- set that pattern specific variable that was previously ignored

- build the prerequisites if needed (with variable set)

- build the file the user asked for (with variable set)

The reason this will not work is because expanding the prerequisites without the variable set results in prerequisites that do not exist, this results in "no rule to make target..." style errors.

Bummer! What now? This is where I left the office to have lunch, I returned about 45 minutes later and drowned myself in pages of the GNU make user manual. I do now know how to make this work, so here it is...

Secondary expansions. One of make's many ugly hacks. 

So, the variable isn't set the first time you expand it, the solution, expand it a second time when it is set. (Really hacky solution but it works.) You want a secondary expansion to happen after the "set pattern specific variable" happens in the flow described above.

To make the first expansion be 'ignored' by make (and leave something that can be expanded later) you use a double dollar before a variable. The first expansion simply removes the first dollar. The rule now looks like this:

%.o: projects/$$(OBJECTNAME)/$$(OBJECTNAME).c
    gcc -c $^ -o $@

To get the second expansion to happen, you simply place this line before the rule and its specific variables;

.SECONDEXPANSION:

Urgh! At least it works, putting it all together, your Makefile now looks like this;

.SECONDEXPANSION: 
%.o: OBJECTNAME = $(basename $@) 
%.o: OBJPREREQ = projects/$(OBJECTNAME)/$(OBJECTNAME).c 

%.o: $$(OBJPREREQ) 
    gcc -c $^ -o $@

Now you start to wonder, how about this makefile...

%.o: projects/$(basename $@)/$(basename $@).c 
    gcc -c $^ -o $@

It should work right? well, maybe...

The following makefile works if you try and build project1.o;

NAME = project1.o
%.o: projects/$(basename $(NAME))/$(basename $(NAME)).c 
    gcc -c $^ -o $@

You don't want this though as you want a generic rule. So you would think that if you remove the NAME variable and just use $@ instead (which should be "project1.o" at the correct point) just like in the example before this one then it should be fine. Nope! Why? That's just make I suppose. $@ simply isn't set when the prerequisites are expanded. It only expands within the build command. This feels odd, this is known at this point and is valid in a pattern specific variable. Allowing this would remove the need for the awful second expansion hack in this case.

So, the solution is this rather ugly thing:

.SECONDEXPANSION: 
%.o: OBJECTNAME = $(basename $@) 
%.o: OBJPREREQ = projects/$(OBJECTNAME)/$(OBJECTNAME).c 

%.o: $$(OBJPREREQ) 
    gcc -c $^ -o $@

(Sigh) Make!

By .

comments powered by Disqus

This website uses cookies. If you don't like this, please stop using this site.