Home > OS >  Loop through subfolders in a parent folder in Makefile
Loop through subfolders in a parent folder in Makefile

Time:10-13

I have a folder containing unknown number of subfolders. Each subfolder contains a Dockerfile. I want to write a Makefile command to batch build all images named after the subfolder and tag them accordingly.

I started with simple trial first:

build.all.images:
    for f in $(find $(image_folder) -type d -maxdepth 1 -mindepth 1 -exec basename {} \;); do echo $${f}; done

When I run the find command separately with value for image_folder hard-coded, subfolders are listed successfully. However, when I tried to run the make command, I only see the output below:

for f in ; do echo ${f}; done

I have also tried to use ls command chained with tf cut to get the list of sub-folders but the result was the same.

build.all.images:
    for f in $(ls -l $(image_folder) | grep '^d' | tr -s ' ' | cut -d ' ' -f 9); do echo $${f}; done

What am I doing something wrong?

CodePudding user response:

Recipes are expanded by make before they are executed by the shell. So $(find something) is expanded and, as there is no make macro named find something, it is replaced by the empty string. Double the $ sign (or use backticks) just like you did for shell variable f:

build.all.images:
    for f in $$(find $(image_folder) -type d -maxdepth 1 -mindepth 1 -exec basename {} \;); do echo $${f}; done

But using a for loop in a make recipe is frequently a not that good idea. Makefiles are not shell scripts. With your solution (after fixing the $ issue) you will not benefit from the power of make. Make analyzes dependencies between targets and prerequisites to redo only what needs to. And it also has parallel capabilities that can be very useful to speed-up your building process.

Here is another more make-ish solution. I changed a bit the logic to find Dockerfiles instead of sub-directories but it is easy to adapt if you prefer the other way.

DOCKERFILES := $(shell find $(image_folder) -type f -name Dockerfile)
TARGETS     := $(patsubst %/Dockerfile,%.done,$(DOCKERFILES))

.PHONY: build.all.images clean

build.all.images: $(TARGETS)

$(TARGETS): %.done: %/Dockerfile
    printf 'sub-directory: %s, Dockerfile: %s\n' "$*" "$<"
    touch $@

clean:
    rm -f $(TARGETS)

Demo:

$ mkdir -p {a..d}
$ touch {a..d}/Dockerfile
$ make -s -j4 build.all.images
sub-directory: c, Dockerfile: c/Dockerfile
sub-directory: a, Dockerfile: a/Dockerfile
sub-directory: d, Dockerfile: d/Dockerfile
sub-directory: b, Dockerfile: b/Dockerfile

With this approach you will rebuild an image only if its Dockerfile changed since the last build. The date/time of the last build of image FOO is the last modification date/time of the empty file named FOO.done that the recipe creates or touches after the build. So, that is less work to do.

Moreover, as the static pattern rule is equivalent to as many independent rules as you have images to build, make can build the outdated images in parallel. Try make -j 8 build.all.images if you have 8 cores and see.

  • Related