make
inherits target-specific variables, so long as they have a unique path, so the following will work:
.PHONY: all
all: all-customer1 all-customer2
.PHONY: all-customer1
all-customer1: CFLAGS=-ggdb3 -O0
all-customer1: $(outdir)customer1/bin/prog
.PHONY: all-customer2
all-customer2: CFLAGS=-O3
all-customer2: $(outdir)customer2/bin/prog
$(outdir)customer1/bin/prog: $(outdir)customer1/src/main.o
$(CC) -o $@ $(CFLAGS) $<
$(outdir)customer2/bin/prog: $(outdir)customer2/src/main.o
$(CC) -o $@ $(CFLAGS) $<
$(outdir)customer1/src/main.o: src/main.c
$(CC) -c -o $@ $(CFLAGS) $^
$(outdir)customer2/src/main.o: src/main.c
$(CC) -c -o $@ $(CFLAGS) $^
meaning that every top-level target gets built using a consistent set of flags, whereas the following doesn't:
.PHONY: all
all: all-customer1 all-customer2
.PHONY: all-customer1
all-customer1: CFLAGS=-ggdb3 -O0
all-customer1: $(outdir)customer1/bin/prog
.PHONY: all-customer2
all-customer2: CFLAGS=-O3
all-customer2: $(outdir)customer2/bin/prog
$(outdir)customer1/bin/prog: src/main.o
$(CC) -o $@ $(CFLAGS) $<
$(outdir)customer2/bin/prog: src/main.o
$(CC) -o $@ $(CFLAGS) $<
src/main.o: src/main.c
$(CC) -c -o $@ $(CFLAGS) $^
because you end up src/main.o
only being compiled once, which is not what you want. So, fundamentally, you must create different physical targets for each toolchain/set of options.
With respect to parsing, how long does it take vs. running the actual build? My experience is that even a very large code base can be parsed into a single heirarchical build tree in a tiny fraction of the time it takes to actually execute the build (even with monsterous build servers...).
So, with these two facts in mind, are you sure that this is what you want to do? Given that you are basically running several builds, surely it makes sense to do just that, especially as it would make it much easier to scale; if you have a new customer, you just add a new build job and if running all those builds takes a long time, you can run them on different servers, e.g. Jenkins slaves.
Sure, it's not as 'clean' and does leave open the posibility of it 'working for some, but not for others', but that would always be a management issue anyway...
If you want to support multiple customer profiles in a nice way, you can do it by using a configuration file which is setup something like this:
# Build config for "Customer X"
# Costomer identifier: customer1
customer1_cflags=-O0 -ggdb3
.PHONY: all-customer1
all-customer1: CFLAGS=$(customer1_cflags)
all-customer1: all
And then, in your Makefile
:
# While convenient, Wildcards are harmful - they make your builds more fragile.
# This, for example, will fail fi there are no configs...
-include $(wildcard configs/*.mk)
This will then enable you to run either make all
to get your stock/reference build or make all-customer1
to run a customised build for your customer - if you've used a variable for specifying your working/output directory, then you could override that on the command line:
make outdir=customer1_build/ all-customer1
Note: variable overrides are based on 'closest' override - if someone has overriden a variable on a rule directly, then this approach will breakdown.
To go beyond this, to create "One Build Tree to Rule them All", you'd need to build a list of sources, then dynamically create the targets using eval. The example below demonstrates the principle, but would need some extra work to make production ready.
Edit: I've included comments between the define ... endef
, but these would need to be removed before the code will function correctly.
The main Makefile
:
BUILD=build/
outdir=$(or $(filter %/,$(strip $(BUILD))),$(strip $(BUILD))/)
program=bin/hello
include platforms/all.mk
include configs/all.mk
# This creates all the necessary constructs for building a
# config/platform combination.
define CreateBuildConfig =
all: all-$(1)-$(2)
.PHONY: all-$(1)-$(2)
all-$(1)-$(2): $(outdir)$(1)/$(2)/$(3)
# Create implicit rule for building sources to objects
$(outdir)$(1)/$(2)/%.o: %.c
$$(CC) -c -o $$@ $$(CFLAGS) $$(DEFINES) $$<
# Set the variables for this config/platform combo...
$(outdir)$(1)/$(2)/$(3): DEFINES=$$(foreach def,$$(platform_$(2)_defines),-D$$(def))
$(outdir)$(1)/$(2)/$(3): CFLAGS=$$(config_$(1)_cflags)
# The rule for creating the executable.
$(outdir)$(1)/$(2)/$(3): $(foreach obj,$(4:.c=.o),$(outdir)$(1)/$(2)/$(obj))
$$(CC) -o $$@ $$(CFLAGS) $$(DEFINES) $$<
# Cleanup after ourselves.
clean: clean-$(1)-$(2)
clean-$(1)-$(2):
-rm -f $(outdir)$(1)/$(2)/$(3)
find $(outdir)$(1)/$(2) -name '*.o' -delete
endef
# Some top-level targets, for documentation purposes.
.PHONY: all
all:
.PHONY: clean
clean:
# Build the list of sources.
sources+=src/main.c
# Create the actual build targets, for each platform/config pair.
$(foreach platform,$(all_platforms),$(foreach config,$(all_configs),$(eval $(call CreateBuildConfig,$(config),$(platform),$(program),$(sources)))))
The platforms config file platforms/all.mk
:
all_platforms:=model1 model2
include platforms/model1.mk
include platforms/model2.mk
Example platform file platforms/model1.mk
:
_platform=model2
platform_$(_platform)_defines:=NAME=$(_platform)
platform_$(_platform)_defines+=MAX_FOO=16
_platform=
The configurations config file config/all.mk
:
all_configs:=gcc48 customer1 customer2
include configs/gcc48.mk
include configs/customer1.mk
include configs/customer2.mk
Example config file customer1.mk
:
_config=customer1
config_$(_config)_cflags=-O3
_config=