makefilegnu-make

Multiple make Rules without targets or prerequisites


I might have the terminology wrong so feel free to correct me. We have been abusing make for sometime to do a lot of our builds. 99% of the time this is using phony targets. We have docker build projects where we call docker build for each of the versioned docker files. The file structure would look something like this:

6/Dockerfile
8/Dockerfile

Getting the list of docker files in the project is easy.

DOCKERFILES:=$(wildcard */Dockerfile)

Currently we are using this approach to build the docker files:

.PHONY: all
all: lint init build-images
.PHONY: lint
lint:
    $(foreach image, $(DOCKERFILES),$(call dockerLint,$(image),$(_DOCKER_BUILD_ARCH)))

.PHONY: init
init:
    $(foreach image, $(DOCKERFILES),$(call initVariablesForImage,$(image)))

.PHONY: build-images
build-images:
    $(foreach image, $(DOCKERFILES),$(call dockerBuildPush,$(image),$(_DOCKER_BUILD_ARCH)))

However in the interest of trying to better understand make and avoid the foreach is it possible to define multiple rules that match the Dockerfile and run the relevant recipes? I have had some success with a single rule but as soon as I try and do multiple recipes it falls over.

.PHONY: test
test: $(DOCKERFILES)

$(DOCKERFILES):*
    $(info ***** build $@)

Which gives the following output:

***** build 6/Dockerfile
***** build 8/Dockerfile

I expect that this approach is wrong based on when I try to introduce other targets I get override warnings or circular references:

warning: overriding commands for target `8/Dockerfile'
warning: ignoring old commands for target `8/Dockerfile'

...

make: Circular 6/Dockerfile <- 6/Dockerfile dependency dropped.
make: Circular 7/Dockerfile <- 7/Dockerfile dependency dropped.
make: Circular 8/Dockerfile <- 8/Dockerfile dependency dropped.

I wanted to stop and pause and see if what I want to achieve is even possible. What we have works but it would be nice to explore alternatives to see if we can come up with cleaner implementations.


Solution

  • Usually when you write a recipe that loops over a number of builds there are chances that you can do much better by letting make handle all builds separately. Example with static pattern rules:

    DOCKERFILES := $(wildcard */Dockerfile)
    LINTS := $(addprefix lint-,$(DOCKERFILES))
    INITS := $(addprefix init-,$(DOCKERFILES))
    BUILDS := $(addprefix build-images-,$(DOCKERFILES))
    
    .PHONY: all lint init build-images $(LINTS) $(INITS) $(BUILDS)
    
    all: lint init build-images
    
    lint: $(LINTS)
    
    $(LINTS): lint-%: %
        $(call dockerLint,$<,$(_DOCKER_BUILD_ARCH))
    
    init: $(INITS)
    
    $(INITS): init-%: %
        $(call initVariablesForImage,$<)
    
    build-images: $(BUILDS)
    
    $(BUILDS): build-images-%: %
        $(call dockerBuildPush,$<,$(_DOCKER_BUILD_ARCH))
    

    One of the advantages is that make can run recipes in parallel, while loops in recipes are sequential. Try make -j12 if you have 12 cores and see...

    Warning: if there are dependencies between the lint, init and build steps they should be specified. Your question does not contain enough information to suggest a solution but if lint shall run before init and init before build, you could adapt the static pattern rules for init and build:

    $(INITS): init-%: % lint-%
        $(call initVariablesForImage,$<)
    
    $(BUILDS): build-images-%: % init-%
        $(call dockerBuildPush,$<,$(_DOCKER_BUILD_ARCH))
    

    Important: as we are using only phony targets everything is rebuilt each time we run make all. But make can do better if it knows the inputs (the files that are used by the recipe) and the products (the files that are created or updated by the recipe) of each rule. With all this make can compare the last modification dates of inputs and products to decide if a recipe must be run or if running it is useless because the products are up to date with respect to the inputs. Your question does not contain enough information to suggest a solution.