Friday, January 27, 2012

Using GNU Make's 'define' and '$(eval)' to automate rule creation

About once a week I get an email from someone random asking a question about GNU Make. I do my best to answer and figured it might be helpful for others if I shared these questions and my answers. So, here goes. This one starts with a Princess Leia style appeal:
I stumbled across your name when googling for help on a GNUmake related problem, hope you have the time to look at it as you are my last hope. I have the following problem in my Makefile:
$(LIBDIR)/libfoo.a: $(filter $(OBJDIR)/foo/%,$(OBJECTS))
$(LIBDIR)/libbar.a: $(filter $(OBJDIR)/bar/%,$(OBJECTS))
$(LIBDIR)/libbaz.a: $(filter $(OBJDIR)/baz/%,$(OBJECTS))
        ar -r [email protected] $^
        ranlib [email protected]
so far, so good - it works as expected. The only problem is, that in the real world it's not just foo, bar and baz for me and I hate having to maintain the list manually. What I would like to do is something like this:
$(LIBDIR)/lib%.a: $(filter $(OBJDIR)/%/%,$(OBJECTS))
        ar -r [email protected] $^
        ranlib [email protected]
but now the pattern for filter has two percentage-characters and this seem to confuse GNUmake. I tried to escape it, use $(call …), etc. but nothing really works. Do you have any trick/hint/idea how to solve this??

Thanks for your time, Best Regards,
And my reply:
Thanks for your mail. I can see what you are trying to do. I think the easiest way out of your predicament is as follows:
# Example of manually maintained list
# $(LIBDIR)/libfoo.a: $(filter $(OBJDIR)/foo/%,$(OBJECTS))
# $(LIBDIR)/libbar.a: $(filter $(OBJDIR)/bar/%,$(OBJECTS))
# $(LIBDIR)/libbaz.a: $(filter $(OBJDIR)/baz/%,$(OBJECTS))
# $(LIBDIR)/%.a:
#        ar -r [email protected] $^
#        ranlib [email protected]

# What you'd like to be able to do
# $(LIBDIR)/lib%.a: $(filter $(OBJDIR)/%/%,$(OBJECTS))
#        ar -r [email protected] $^
#        ranlib [email protected]

# The following will work
# Suppose, for example:

LIBDIR  := lib
OBJDIR  := obj
OBJECTS := $(OBJDIR)/foo/hello.o $(OBJDIR)/foo/bar.o $(OBJDIR)/bar/hello.o \

# Extract the names of the first level directories underneath
# $(OBJDIR) and make a unique list (sort removes duplicates) and store
# that in LIBNAMES.  This assumes that there are no spaces in any of
# the filenames in $(OBJECTS) and that each element of $(OBJECTS)
# starts with $(OBJDIR)

LIBNAMES := $(sort $(foreach a,$(patsubst $(OBJDIR)/%,%,$(OBJECTS)),\
$(firstword $(subst /, ,$a))))

# The following function finds all the objects in $(OBJECTS) in a
# particular directory whose name can be found in LIBNAMES.  So, in
# the example here doing $(call find-objects,foo) would return
# obj/foo/hello.o obj/foo/bar.o

find-objects = $(filter $(OBJDIR)/$1/%,$(OBJECTS))

# Now need to define the rule that handles each of the libraries
# mentioned in $(LIBNAMES).  This function can be used with $(eval) to
# define the rule for a single directory

define make-library 
$(LIBDIR)/lib$1.a: $(call find-objects,$1)
        ar -r [email protected] $$^
        ranlib [email protected]

# Now define the rules for all the directories found in $(LIBNAMES)

$(foreach d,$(LIBNAMES),$(eval $(call make-library,$d)))
You will need a version of GNU Make that supports $(eval).

1 comment:

Josh Enders said...

Thanks JGC!