I have a project with multiple Dockerfiles, each located in a named directory that I use as the Docker image tag. I need to build, test, and push each a docker image for each Dockerfile. What do I need to do to make something such as the following work with GNU Make?
# BUILDS needs to be a list of directories, not a list of Dockerfiles
BUILDS := $(wildcard */Dockerfile)
VERSION := $(shell git rev-parse --short=12 --verify HEAD)
DOCKER_REPO_URL := quay.io/reponame
define docker_build =
$(1):
#echo "Building $$#"
docker build -t $$# --force-rm $$#
endef
define docker_test =
$(1):
#echo "Testing $$#"
docker inspect $$#
docker run --rm $$# help
endef
define docker_push =
$(1):
#echo "Pushing $$#"
docker tag $$# $(DOCKER_REPO_URL):$$#-$(VERSION)
docker push $(DOCKER_REPO_URL):$$#-$(VERSION)
docker tag $$# $(DOCKER_REPO_URL):$$#
docker push $(DOCKER_REPO_URL):$$#
endef
.PHONY: all build test release clean
all: build test release
build: $(BUILDS)
$(foreach build,$(BUILDS),$(eval $(call docker_build,$(build))))
test: $(BUILDS)
$(foreach test,$(BUILDS),$(eval $(call docker_test,$(test))))
release:
$(foreach image,$(BUILDS),$(eval $(call docker_push,$(image))))
I'm not sure this is what you want, but...
First consider the BUILD variable. If we have three Dockerfiles:
foo/Dockerfile
bar/Dockerfile
baz/Dockerfile
then we want BUILDS to contain foo bar baz
Here are a couple of attempts:
BUILDS := $(wildcard */Dockerfile) # this is foo/Dockerfile bar/Dockerfile baz/Dockerfile
BUILDS := $(dir $(wildcard */Dockerfile)) # this is foo/ bar/ baz/
BUILDS := $(patsubst %/,%, $(dir $(wildcard */Dockerfile))) # this is foo bar baz
Crude but effective.
Now the rules. Ordinarily the target of a rule is the name of a file which the rule builds. In this case we must break this convention, since we don't know what the name of the image file will be. So if the directory is foo/, we could have a rule called build_foo:
build_foo:
#echo "Building foo"
#echo docker build -t foo --force-rm foo
Since we don't want to write a rule for every possible directory, we will use automatic variables and create a pattern rule:
build_%:
#echo "Building $$#"
#echo docker build -t $* --force-rm $*
Now "make build_foowill work correctly. And we could write abuild` rule that builds all of them:
build: $(addprefix build_,$(BUILDS))
But this is not quite the right approach. We want to build, then test, then push each image, in that order. So we'd like something like this:
push_foo: test_foo
test_foo: build_foo
We can do this with pattern rules:
test_%: build_%
...
push_%: test_%
...
release: $(addprefix push_,$(BUILDS))
Now "make release" will do everything. (And if you put release: as the first rule in the makefile, it will be the default rule, and "make" will suffice.)
Like #Beta, I don't see why you want to build all the images, then
test all the images, then push all the images, as opposed to
building, testing and pushing each image; and the latter approach lends itself
to a simpler and more normal makefile.
If you have reasons for having to do it the first way, then you'd need a
makefile something like this:
# Assuming each subdirectory `foobar` containing a Dockerfile
# is where we `docker build` the image `foobar`
IMAGES := $(patsubst %/,%,$(dir $(wildcard */Dockerfile)))
BUILD_TARGS = $(patsubst %,build_%,$(IMAGES))
TEST_TARGS = $(patsubst %,test_%,$(IMAGES))
PUSH_TARGS = $(patsubst %,push_%,$(IMAGES))
VERSION := 1 # $(shell git rev-parse --short=12 --verify HEAD)
DOCKER_REPO_URL := quay.io/reponame
define docker_build =
build_$(1):
#echo "Building $(1)"
#docker build -t $(1) --force-rm $(1)
endef
define docker_test =
test_$(1):
#echo "Testing $(1)"
#docker inspect $(1)
#docker run --rm $(1) help
endef
define docker_push =
push_$(1):
#echo "Pushing $(1)"
#docker tag $(1) $(DOCKER_REPO_URL):$(1)-$(VERSION)
#docker push $(DOCKER_REPO_URL):$(1)-$(VERSION)
#docker tag $$# $(DOCKER_REPO_URL):$(1)
#docker push $(DOCKER_REPO_URL):$(1)
endef
.PHONY: all build test release clean $(IMAGES) $(BUILD_TARGS) $(TEST_TARGS) $(PUSH_TARGS)
all: build test release
build: $(BUILD_TARGS)
test: $(TEST_TARGS)
release: $(PUSH_TARGS)
$(foreach image,$(IMAGES),$(eval $(call docker_build,$(image))))
$(foreach image,$(IMAGES),$(eval $(call docker_test,$(image))))
$(foreach image,$(IMAGES),$(eval $(call docker_push,$(image))))
Obviously, uncomment the docker commands to make them run, and restore the
correct definition of VERSION.
As it stands it will give you the like of:
$ make
Building foo
#docker build -t foo --force-rm foo
Building bar
#docker build -t bar --force-rm bar
Testing foo
#docker inspect foo
#docker run --rm foo help
Testing bar
#docker inspect bar
#docker run --rm bar help
Pushing foo
#docker tag foo quay.io/reponame:foo-1
#docker push quay.io/reponame:foo-1
#docker tag push_foo quay.io/reponame:foo
#docker push quay.io/reponame:foo
Pushing bar
#docker tag bar quay.io/reponame:bar-1
#docker push quay.io/reponame:bar-1
#docker tag push_bar quay.io/reponame:bar
#docker push quay.io/reponame:bar
Related
how good/bad practice is it to have "nonconstant" targets in a makefile?
The company I work for produces embedded equipment, and I'm a firmware engineer.
The coding rules of the company I work for require that files produced by a build must have a name tagged with the version and also with a hash of information from a git repository (for test and production department needs).
An additional constraint is that a build is produced with a single command.
That's why I'm faced with makefiles every day whose targets have names that can change from build to build.
I wonder if these are common problems or not.I wonder if the example I wrote is a good practice to solve it:
# This is just an example to (hopefully) help to understand.
define populate_variable_with_remote_file_content
$(if $(findstring $(origin $(1)),undefined), \
$(eval $(1) = $(shell git archive master --remote=git#gitserver:project.git $(2) | tar -x --to-stdout)) \
)
endef
.PHONY: clean populate_TAG
all: populate_TAG
$(eval TARGET = dummy_$(TAG))
$(eval export TAG TARGET)
$(MAKE) -f $(firstword $(MAKEFILE_LIST)) $(TARGET)
$(TARGET):
echo hallo > $#
populate_TAG:
$(eval $(call populate_variable_with_remote_file_content,TAG,somefile))
clean:
rm dummy_*
I wonder if there are other (better) ways to solve this.
I'm using similar code to https://stackoverflow.com/a/17845120/1375972 which includes:
TOPTARGETS := zip test clean
SUBDIRS := $(wildcard */.)
$(TOPTARGETS): $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $# $(MAKECMDGOALS)
This runs whichever make command is passed (from TOPTARGETS) on every subdirectory, which is what I want.
However I want to have a target deploy which has this same behaviour only when an environment variable is set. Here's what I've tried:
deploy:
ifdef GITLAB_CI
#echo "GITLAB_CI variable is defined, running deploy on all subdirectories"
$(MAKE) -C $(SUBDIRS) $(MAKECMDGOALS)
else
#echo "snip"
endif
Notice the $(MAKE) line is the same as the $(SUBDIRS) one, just with the $# replaced with $(SUBDIRS) directly.
So the logic is when it runs in my CI it'll run the deploy recursively on all subdirectories, but when it's run locally it doesn't. The problem is that the $(SUBDIRS) in deploy doesn't behave as expected. When I run make deploy in a directory with 2 subdirectories:
> make deploy
GITLAB_CI variable is defined, running deploy on all subdirectories
/Library/Developer/CommandLineTools/usr/bin/make -C subdir1/. subdir2/. deploy
make[1]: *** No rule to make target `subdir2/.'. Stop.
make: *** [deploy] Error 2
Compared to make clean (one of my TOPTARGETS):
> make clean
/Library/Developer/CommandLineTools/usr/bin/make -C subdir1/. clean
/Library/Developer/CommandLineTools/usr/bin/make -C subdir2/. clean
So when the TOPTARGETS are used, the $# seems to unroll $(SUBDIRS) in a different way to when I write the same line myself with $# substituted to $(SUBDIRS). Is there any way to get that behaviour myself in the deploy line? Or do I need to write my own for loop over $(SUBDIRS) inside that target?
You could keep what works and add a minimal deploy rule:
TOPTARGETS := zip test clean
SUBDIRS := $(wildcard */.)
$(TOPTARGETS): $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $# $(MAKECMDGOALS)
ifdef GITLAB_CI
deploy: $(SUBDIRS)
else
deploy:
#echo "snip"
endif
Or, maybe even simpler:
TOPTARGETS := zip test clean
ifdef GITLAB_CI
TOPTARGETS += deploy
else
deploy:
#echo "snip"
endif
SUBDIRS := $(wildcard */.)
$(TOPTARGETS): $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $# $(MAKECMDGOALS)
Suppose I have a project with two or more subfolders foo, bar, etc. I have a Makefile at the root of the project, and also in each subdirectory.
I would like to have certain targets (e.g. all, clean, etc) to run recursively in each subdirectory. My top-level Makefile looks like this:
all:
$(MAKE) -C foo all
$(MAKE) -C bar all
clean:
$(MAKE) -C foo clean
$(MAKE) -C bar clean
Seems to me there's a lot of duplication going on here. Is there a way I can avoid such tedious duplication in my Makefiles?
How about this:
SUBDIRS=foo bar
all clean:
for dir in $(SUBDIRS) ; do \
$(MAKE) -C $$dir $# ; \
done
A bit scary:
SUBDIRS=foo bar
SUBDIR_TARGETS=all clean
define subdir_rule
$(2): $(1)-$(2)
$(1)-$(2):
make -C $(1) $(2)
endef
$(foreach targ,$(SUBDIR_TARGETS),\
$(foreach dir,$(SUBDIRS),\
$(eval $(call subdir_rule,$(dir),$(targ)))))
Here's how I'd do it:
SUBDIRS=foo bar baz
TARGETS = clean all whatever
.PHONY:$(TARGETS)
# There really should be a way to do this as "$(TARGETS):%:TARG=%" or something...
all: TARG=all
clean: TARG=clean
whatever: TARG=whatever
$(TARGETS): $(SUBDIRS)
#echo $# done
.PHONY: $(SUBDIRS)
$(SUBDIRS):
#$(MAKE) -s -C $# $(TARG)
The following code is taken from OpenWRT project. Can someone give an abstract description? Thanks in advance!
#
# Copyright (C) 2007 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
SUBTARGETS:=clean download prepare compile install update refresh prereq dist distcheck
subtarget-default = $(filter-out ., \
$(if $($(1)/builddirs-$(2)),$($(1)/builddirs-$(2)), \
$(if $($(1)/builddirs-default),$($(1)/builddirs-default), \
$($(1)/builddirs))))
define subtarget
$(call warn_eval,$(1),t,T,$(1)/$(2): $($(1)/) $(foreach bd,$(call subtarget-default,$(1),$(2)),$(1)/$(bd)/$(2)))
endef
lastdir=$(word $(words $(subst /, ,$(1))),$(subst /, ,$(1)))
diralias=$(if $(findstring $(1),$(call lastdir,$(1))),,$(call lastdir,$(1)))
# Parameters: <subdir>
define subdir
$(call warn,$(1),d,D $(1))
$(foreach bd,$($(1)/builddirs),
$(call warn,$(1),d,BD $(1)/$(bd))
$(foreach target,$(SUBTARGETS),
$(foreach btype,$(buildtypes-$(bd)),
$(call warn_eval,$(1)/$(bd),t,T,$(1)/$(bd)/$(btype)/$(target): $(if $(QUILT),,$($(1)/$(bd)/$(btype)/$(target)) $(call $(1)//$(btype)/$(target),$(1)/$(bd)/$(btype))))
+$$(SUBMAKE) -C $(1)/$(bd) $(btype)-$(target) $(if $(findstring $(bd),$($(1)/builddirs-ignore-$(btype)-$(target))), || $(call MESSAGE, ERROR: $(1)/$(bd) [$(btype)] failed to build.))
$$(if $(call debug,$(1)/$(bd),v),,.SILENT: $(1)/$(bd)/$(btype)/$(target))
$(if $(call diralias,$(bd)),$(call warn_eval,$(1)/$(bd),l,T,$(1)/$(call diralias,$(bd))/$(btype)/$(target): $(1)/$(bd)/$(btype)/$(target)))
)
$(call warn_eval,$(1)/$(bd),t,T,$(1)/$(bd)/$(target): $(if $(QUILT),,$($(1)/$(bd)/$(target)) $(call $(1)//$(target),$(1)/$(bd))))
$(if $(BUILD_LOG),#mkdir -p $(BUILD_LOG_DIR)/$(1)/$(bd))
$(foreach variant,$(if $(BUILD_VARIANT),$(BUILD_VARIANT),$(if $($(1)/$(bd)/variants),$($(1)/$(bd)/variants),__default)),
+$(if $(BUILD_LOG),set -o pipefail;) $$(SUBMAKE) -C $(1)/$(bd) $(target) BUILD_VARIANT="$(filter-out __default,$(variant))" $(if $(BUILD_LOG),SILENT= 2>&1 | tee $(BUILD_LOG_DIR)/$(1)/$(bd)/$(target).txt) $(if $(findstring $(bd),$($(1)/builddirs-ignore-$(target))), || $(call MESSAGE, ERROR: $(1)/$(bd) failed to build$(if $(filter-out __default,$(variant)), (build variant: $(variant))).))
)
$$(if $(call debug,$(1)/$(bd),v),,.SILENT: $(1)/$(bd)/$(target))
# legacy targets
$(call warn_eval,$(1)/$(bd),l,T,$(1)/$(bd)-$(target): $(1)/$(bd)/$(target))
# aliases
$(if $(call diralias,$(bd)),$(call warn_eval,$(1)/$(bd),l,T,$(1)/$(call diralias,$(bd))/$(target): $(1)/$(bd)/$(target)))
)
)
$(foreach target,$(SUBTARGETS),$(call subtarget,$(1),$(target)))
endef
# Parameters: <subdir> <name> <target> <depends> <config options> <stampfile location>
define stampfile
$(1)/stamp-$(3):=$(if $(6),$(6),$(STAGING_DIR))/stamp/.$(2)_$(3)$(if $(5),_$(call confvar,$(5)))
$$($(1)/stamp-$(3)): $(TMP_DIR)/.build $(4)
#+$(SCRIPT_DIR)/timestamp.pl -n $$($(1)/stamp-$(3)) $(1) $(4) || \
$(MAKE) $$($(1)/flags-$(3)) $(1)/$(3)
#mkdir -p $$$$(dirname $$($(1)/stamp-$(3)))
#touch $$($(1)/stamp-$(3))
$$(if $(call debug,$(1),v),,.SILENT: $$($(1)/stamp-$(3)))
.PRECIOUS: $$($(1)/stamp-$(3)) # work around a make bug
$(1)//clean:=$(1)/stamp-$(3)/clean
$(1)/stamp-$(3)/clean: FORCE
#rm -f $$($(1)/stamp-$(3))
endef
The subdir template generates the standard make targets for a subdirectory.
It is called with the directory name as first argument, e.g. in package/Makefile it's 'package'
it then treats the variable package/builddirs as the list of subdirectories with valid build targets, which are contained in the SUBTARGETS variable.
For running the 'compile' target of a package 'foo', it allows you to run 'make package/foo/compile' which will recursively invoke make with the right environment variables in order to build the subdirectory.
This template also has support for some optional features that can be enabled through environment variables, e.g. for logging the output to a file, ignoring dependencies, etc.
It also supports selective debugging output.
The stampfile template is simply a small makefile target wrapper for speeding up builds by keeping a recursively timestamp-checked stampfile around it
I want to create directories using makefile. My project directory is like this
+--Project
+--output
+--source
+Testfile.cpp
+Makefile
I want to put all the objects and output into the respective output folder. I want to create folder structure which would be like this after compiling.
+--Project
+--output
+--debug (or release)
+--objs
+Testfile.o
+Testfile (my executable file)
+--source
+Testfile.cpp
+Makefile
I tried with several options, but could not succeed. Please help me to make directories using make file. I'm posting my Makefile for your consideration.
#---------------------------------------------------------------------
# Input dirs, names, files
#---------------------------------------------------------------------
OUTPUT_ROOT := output/
TITLE_NAME := TestProj
ifdef DEBUG
TITLE_NAME += _DEBUG
else
ifdef RELEASE
TITLE_NAME += _RELEASE
endif
endif
# Include all the source files here with the directory tree
SOURCES := \
source/TestFile.cpp \
#---------------------------------------------------------------------
# configs
#---------------------------------------------------------------------
ifdef DEBUG
OUT_DIR := $(OUTPUT_ROOT)debug
CC_FLAGS := -c -Wall
else
ifdef RELEASE
OUT_DIR := $(OUTPUT_ROOT)release
CC_FLAGS := -c -Wall
else
$(error no build type defined)
endif
endif
# Put objects in the output directory.
OUT_O_DIR := $(OUT_DIR)/objs
#---------------------------------------------------------------------
# settings
#---------------------------------------------------------------------
OBJS = $(SOURCES:.cpp=.o)
DIRS = $(subst /,/,$(sort $(dir $(OBJS))))
DIR_TARGET = $(OUT_DIR)
OUTPUT_TARGET = $(OUT_DIR)/$(TITLE_NAME)
CC_FLAGS +=
LCF_FLAGS :=
LD_FLAGS :=
#---------------------------------------------------------------------
# executables
#---------------------------------------------------------------------
MD := mkdir
RM := rm
CC := g++
#---------------------------------------------------------------------
# rules
#---------------------------------------------------------------------
.PHONY: all clean title
all: title
clean:
$(RM) -rf $(OUT_DIR)
$(DIR_TARGET):
$(MD) -p $(DIRS)
.cpp.o:
#$(CC) -c $< -o $#
$(OBJS): $(OUT_O_DIR)/%.o: %.cpp
#$(CC) -c $< -o $#
title: $(DIR_TARGET) $(OBJS)
In my opinion, directories should not be considered targets of your makefile, either in technical or in design sense. You should create files and if a file creation needs a new directory then quietly create the directory within the rule for the relevant file.
If you're targeting a usual or "patterned" file, just use make's internal variable $(#D), that means "the directory the current target resides in" (cmp. with $# for the target). For example,
$(OUT_O_DIR)/%.o: %.cpp
#mkdir -p $(#D)
#$(CC) -c $< -o $#
title: $(OBJS)
Then, you're effectively doing the same: create directories for all $(OBJS), but you'll do it in a less complicated way.
The same policy (files are targets, directories never are) is used in various applications. For example, git revision control system doesn't store directories.
Note: If you're going to use it, it might be useful to introduce a convenience variable and utilize make's expansion rules.
dir_guard=#mkdir -p $(#D)
$(OUT_O_DIR)/%.o: %.cpp
$(dir_guard)
#$(CC) -c $< -o $#
$(OUT_O_DIR_DEBUG)/%.o: %.cpp
$(dir_guard)
#$(CC) -g -c $< -o $#
title: $(OBJS)
This would do it - assuming a Unix-like environment.
MKDIR_P = mkdir -p
.PHONY: directories
all: directories program
directories: ${OUT_DIR}
${OUT_DIR}:
${MKDIR_P} ${OUT_DIR}
This would have to be run in the top-level directory - or the definition of ${OUT_DIR} would have to be correct relative to where it is run. Of course, if you follow the edicts of Peter Miller's "Recursive Make Considered Harmful" paper, then you'll be running make in the top-level directory anyway.
I'm playing with this (RMCH) at the moment. It needed a bit of adaptation to the suite of software that I am using as a test ground. The suite has a dozen separate programs built with source spread across 15 directories, some of it shared. But with a bit of care, it can be done. OTOH, it might not be appropriate for a newbie.
As noted in the comments, listing the 'mkdir' command as the action for 'directories' is wrong. As also noted in the comments, there are other ways to fix the 'do not know how to make output/debug' error that results. One is to remove the dependency on the the 'directories' line. This works because 'mkdir -p' does not generate errors if all the directories it is asked to create already exist. The other is the mechanism shown, which will only attempt to create the directory if it does not exist. The 'as amended' version is what I had in mind last night - but both techniques work (and both have problems if output/debug exists but is a file rather than a directory).
Or, KISS.
DIRS=build build/bins
...
$(shell mkdir -p $(DIRS))
This will create all the directories after the Makefile is parsed.
make in, and off itself, handles directory targets just the same as file targets. So, it's easy to write rules like this:
outDir/someTarget: Makefile outDir
touch outDir/someTarget
outDir:
mkdir -p outDir
The only problem with that is, that the directories timestamp depends on what is done to the files inside. For the rules above, this leads to the following result:
$ make
mkdir -p outDir
touch outDir/someTarget
$ make
touch outDir/someTarget
$ make
touch outDir/someTarget
$ make
touch outDir/someTarget
This is most definitely not what you want. Whenever you touch the file, you also touch the directory. And since the file depends on the directory, the file consequently appears to be out of date, forcing it to be rebuilt.
However, you can easily break this loop by telling make to ignore the timestamp of the directory. This is done by declaring the directory as an order-only prerequsite:
# The pipe symbol tells make that the following prerequisites are order-only
# |
# v
outDir/someTarget: Makefile | outDir
touch outDir/someTarget
outDir:
mkdir -p outDir
This correctly yields:
$ make
mkdir -p outDir
touch outDir/someTarget
$ make
make: 'outDir/someTarget' is up to date.
TL;DR:
Write a rule to create the directory:
$(OUT_DIR):
mkdir -p $(OUT_DIR)
And have the targets for the stuff inside depend on the directory order-only:
$(OUT_DIR)/someTarget: ... | $(OUT_DIR)
All solutions including the accepted one have some issues as stated in their respective comments. The accepted answer by #jonathan-leffler is already quite good but does not take into effect that prerequisites are not necessarily to be built in order (during make -j for example). However simply moving the directories prerequisite from all to program provokes rebuilds on every run AFAICT.
The following solution does not have that problem and AFAICS works as intended.
MKDIR_P := mkdir -p
OUT_DIR := build
.PHONY: directories all clean
all: $(OUT_DIR)/program
directories: $(OUT_DIR)
$(OUT_DIR):
${MKDIR_P} $(OUT_DIR)
$(OUT_DIR)/program: | directories
touch $(OUT_DIR)/program
clean:
rm -rf $(OUT_DIR)
I've just come up with a fairly reasonable solution that lets you define the files to build and have directories be automatically created. First, define a variable ALL_TARGET_FILES that holds the file name of every file that your makefile will be build. Then use the following code:
define depend_on_dir
$(1): | $(dir $(1))
ifndef $(dir $(1))_DIRECTORY_RULE_IS_DEFINED
$(dir $(1)):
mkdir -p $$#
$(dir $(1))_DIRECTORY_RULE_IS_DEFINED := 1
endif
endef
$(foreach file,$(ALL_TARGET_FILES),$(eval $(call depend_on_dir,$(file))))
Here's how it works. I define a function depend_on_dir which takes a file name and generates a rule that makes the file depend on the directory that contains it and then defines a rule to create that directory if necessary. Then I use foreach to call this function on each file name and eval the result.
Note that you'll need a version of GNU make that supports eval, which I think is versions 3.81 and higher.
given that you're a newbie, I'd say don't try to do this yet. it's definitely possible, but will needlessly complicate your Makefile. stick to the simple ways until you're more comfortable with make.
that said, one way to build in a directory different from the source directory is VPATH; i prefer pattern rules
OS independence is critical for me, so mkdir -p is not an option. I created this series of functions that use eval to create directory targets with the prerequisite on the parent directory. This has the benefit that make -j 2 will work without issue since the dependencies are correctly determined.
# convenience function for getting parent directory, will eventually return ./
# $(call get_parent_dir,somewhere/on/earth/) -> somewhere/on/
get_parent_dir=$(dir $(patsubst %/,%,$1))
# function to create directory targets.
# All directories have order-only-prerequisites on their parent directories
# https://www.gnu.org/software/make/manual/html_node/Prerequisite-Types.html#Prerequisite-Types
TARGET_DIRS:=
define make_dirs_recursively
TARGET_DIRS+=$1
$1: | $(if $(subst ./,,$(call get_parent_dir,$1)),$(call get_parent_dir,$1))
mkdir $1
endef
# function to recursively get all directories
# $(call get_all_dirs,things/and/places/) -> things/ things/and/ things/and/places/
# $(call get_all_dirs,things/and/places) -> things/ things/and/
get_all_dirs=$(if $(subst ./,,$(dir $1)),$(call get_all_dirs,$(call get_parent_dir,$1)) $1)
# function to turn all targets into directories
# $(call get_all_target_dirs,obj/a.o obj/three/b.o) -> obj/ obj/three/
get_all_target_dirs=$(sort $(foreach target,$1,$(call get_all_dirs,$(dir $(target)))))
# create target dirs
create_dirs=$(foreach dirname,$(call get_all_target_dirs,$1),$(eval $(call make_dirs_recursively,$(dirname))))
TARGETS := w/h/a/t/e/v/e/r/things.dat w/h/a/t/things.dat
all: $(TARGETS)
# this must be placed after your .DEFAULT_GOAL, or you can manually state what it is
# https://www.gnu.org/software/make/manual/html_node/Special-Variables.html
$(call create_dirs,$(TARGETS))
# $(TARGET_DIRS) needs to be an order-only-prerequisite
w/h/a/t/e/v/e/r/things.dat: w/h/a/t/things.dat | $(TARGET_DIRS)
echo whatever happens > $#
w/h/a/t/things.dat: | $(TARGET_DIRS)
echo whatever happens > $#
For example, running the above will create:
$ make
mkdir w/
mkdir w/h/
mkdir w/h/a/
mkdir w/h/a/t/
mkdir w/h/a/t/e/
mkdir w/h/a/t/e/v/
mkdir w/h/a/t/e/v/e/
mkdir w/h/a/t/e/v/e/r/
echo whatever happens > w/h/a/t/things.dat
echo whatever happens > w/h/a/t/e/v/e/r/things.dat
See https://www.oreilly.com/library/view/managing-projects-with/0596006101/ch12.html
REQUIRED_DIRS = ...
_MKDIRS := $(shell for d in $(REQUIRED_DIRS); \
do \
[[ -d $$d ]] || mkdir -p $$d; \
done)
$(objects) : $(sources)
As I use Ubuntu, I also needed add this at the top of my Makefile:
SHELL := /bin/bash # Use bash syntax
I use the makefiles in windows environment and my simple solution is as follows,
Create a target makedir and add it as a prerequisites to where ever it is required.
# Default goal
all: gccversion makedir build finalize list sizeafter completed
The makedir target is (applicable only in windows environment)
makedir:
#IF NOT EXIST $(subst /,\,$(BUILD_DIR)) mkdir $(subst /,\,$(BUILD_DIR)) 2> NULL
#IF NOT EXIST $(subst /,\,$(OUTPUT_DIR)) mkdir $(subst /,\,$(OUTPUT_DIR)) 2> NULL
#IF NOT EXIST $(subst /,\,$(DEP_DIR)) mkdir $(subst /,\,$(DEP_DIR)) 2> NUL
#IF NOT EXIST $(subst /,\,$(OBJ_DIR)) mkdir $(subst /,\,$(OBJ_DIR)) 2> NUL
$(subst /,\,$(BUILD_DIR)) converts the directory separator / to \ and
mkdir $(subst /,\,$(BUILD_DIR)) 2> NUL redirects the error if any.
src_dir := src
obj_dir := obj
build_dir := build
dirs := $(src_dir) $(obj_dir) $(build_dir) # new variable
all: $(dirs) $(other_dependencies) # added dependency (*before* any others)
$(dirs): # rule which makes missing directories
mkdir $#
Won't clutter your terminal with "cannot create directory" error messages. If the directories exist, they don't need to be built.
Works like any other dependency, only requires one rule and one variable.