Passing target variables from pattern match to static - makefile

I'm writing a Makefile to wrap the deployment of an elastic beanstalk app to multiple environments. This is my first time writing an "advanced" makefile and I'm having a bit of trouble.
My goals are as follows:
make deploy by itself should deploy with ENVIRONMENT=$(USER) and ENV_TYPE=staging.
The environment to deploy to can be set via an environment variable. If ENVIRONMENT=production then ENV_TYPE=production, otherwise ENV_TYPE=staging.
As a shorthand for setting the environment variable, one can suffix the deploy target with a - and the name of the environment. For example: make deploy-production.
It's number 3 that is giving me the most trouble. Since any environment not named production is of type staging, I tried to use pattern matching to define the target for deploying to staging environments. Here's what I have currently:
ENVIRONMENT = $(USER)
ifeq ($ENVIRONMENT, production)
ENV_TYPE=production
else
ENV_TYPE=staging
endif
DOCKER_TAG ?= $(USER)
CONTAINER_PORT ?= 8000
ES_HOST = logging-ingest.$(ENV_TYPE).internal:80
.PHONY: deploy
deploy:
-pip install --upgrade awsebcli
sed "s/<<ES_HOST>>/$(ES_HOST)/" < 01-filebeat.template > .ebextensions/01-filebeat.config
sed "s/<<DOCKER_TAG>>/$(DOCKER_TAG)/" < Dockerrun.template | sed "s/<<CONTAINER_PORT>>/$(CONTAINER_PORT)/" > Dockerrun.aws.json
eb labs cleanup-versions --num-to-leave 10 --older-than 5 --force -v --region us-west-2
eb deploy -v coolapp-$(ENVIRONMENT)
.PHONY: deploy-%
deploy-%: ENVIRONMENT=$*
deploy-%: deploy
#echo # Noop required
.PHONY: deploy-production
deploy-production: ENVIRONMENT=production
deploy-production: ENV_TYPE=production
deploy-production: deploy
#echo # Noop required
The problem is in the last step of the deploy target. Namely, $(ENVIRONMENT) appears to be unset.
Example Output:
18:42 $ make -n deploy-staging
pip install --upgrade awsebcli
sed "s/<<ES_HOST>>/logging-ingest.staging.internal:80/" < 01-filebeat.template > .ebextensions/01-filebeat.config
sed "s/<<DOCKER_TAG>>/schultjo/" < Dockerrun.template | sed "s/<<CONTAINER_PORT>>/8000/" > Dockerrun.aws.json
eb labs cleanup-versions --num-to-leave 10 --older-than 5 --force -v --region us-west-2
eb deploy -v coolapp-
echo # Noop required
Desired Output:
18:42 $ make -n deploy-staging
pip install --upgrade awsebcli
sed "s/<<ES_HOST>>/logging-ingest.staging.internal:80/" < 01-filebeat.template > .ebextensions/01-filebeat.config
sed "s/<<DOCKER_TAG>>/schultjo/" < Dockerrun.template | sed "s/<<CONTAINER_PORT>>/8000/" > Dockerrun.aws.json
eb labs cleanup-versions --num-to-leave 10 --older-than 5 --force -v --region us-west-2
eb deploy -v coolapp-staging
echo # Noop required
So I have tried to implement Renaud's recursive Make solution, but have run into a minor hiccup. Here's a simplified Makefile that I'm using:
ENVIRONMENT ?= $(USER)
ifeq ($ENVIRONMENT, production)
ENV_TYPE = production
else
ENV_TYPE = staging
endif
ES_HOST = logging-ingest.$(ENV_TYPE).internal:80
.PHONY: deploy
deploy:
#echo $(ENVIRONMENT)
#echo $(ENV_TYPE)
#echo $(ES_HOST)
.PHONY: deploy-%
deploy-%:
#$(MAKE) --no-print-directory ENVIRONMENT=$* deploy
When I run make production, it looks like the if statements surrounding the ENV_TYPE definition are not run again. Here's my actual output:
12:50 $ make -n deploy-production
/Applications/Xcode.app/Contents/Developer/usr/bin/make --no-print-directory ENVIRONMENT=production deploy
echo production
echo staging
echo logging-ingest.staging.internal:80
The last two lines should say production rather than staging, implying there is something wrong with my conditional, but I haven't edited that conditional from earlier versions when it worked, so I'm a but confused. The same error happens if I invoke make with ENVIRONMENT set manually (e.g. ENVIRONMENT=production make deploy).

Your problem comes from the way target-specific variable values are inherited by target pre-requisites. What you are trying to do works for explicit target-specific variables:
$ cat Makefile
ENVIRONMENT = default
deploy:
#echo '$#: ENVIRONMENT = $(ENVIRONMENT)'
deploy-foo: ENVIRONMENT = foo
deploy-foo: deploy
#echo '$#: ENVIRONMENT = $(ENVIRONMENT)'
$ make deploy
deploy: ENVIRONMENT = default
$ make deploy-foo
deploy: ENVIRONMENT = foo
deploy-foo: ENVIRONMENT = foo
because the deploy-foo-specific ENVIRONMENT variable is assigned value foo during the first expansion phase (the one during which target-specific variable assignments, target lists and pre-requisite lists are expanded). So, deploy inherits this value.
But it does not work with your pattern-specific variables that use the $* automatic variable:
$ cat Makefile
ENVIRONMENT = default
deploy:
#echo '$#: ENVIRONMENT = $(ENVIRONMENT)'
deploy-%: ENVIRONMENT = $*
deploy-%: deploy
#echo '$#: ENVIRONMENT = $(ENVIRONMENT)'
$ make deploy
$ deploy: ENVIRONMENT = default
$ make deploy-foo
deploy: ENVIRONMENT =
deploy-foo: ENVIRONMENT = foo
The reason is that the deploy-foo-specific ENVIRONMENT variable is what is called a recursively expanded variable in make dialect (because you use the = assignment operator). It is expanded, recursively, but only when make needs its value, not when it is assigned. So, in the context of deploy-foo, it is assigned $*, not the pattern stem. ENVIRONMENT is passed as is to the context of the deploy pre-requisite and, in this context, $(ENVIRONMENT) is recursively expanded to $* and then to the empty string because there is no pattern in the deploy rule. You could try the simply expanded (non-recursive) variable flavour:
deploy-%: ENVIRONMENT := $*
that is immediately expanded, but the result would be the same because $* expands as the empty string during the first expansion. It is set only during the second expansion and can thus be used only in recipes (that make expands in a second phase).
A simple (but not super-efficient) solution consists in invoking make again:
deploy-%:
#$(MAKE) ENVIRONMENT=$* deploy
Example:
$ cat Makefile
ENVIRONMENT = default
deploy:
#echo '$(ENVIRONMENT)'
deploy-%:
#$(MAKE) --no-print-directory ENVIRONMENT=$* deploy
$ make
default
$ make deploy
default
$ make deploy-foo
foo
Note: GNU make supports a secondary expansion and one could think that it can be used to solve this problem. Unfortunately not: the secondary expansion expands only the pre-requisites, not the target-specific variable definitions.
As mentioned above, this recursive make is not very efficient. If efficiency is critical one single make invocation is preferable. And this can be done if you move all your variables processing in the recipe of a single pattern rule. Example if the shell that make uses is bash:
$ cat Makefile
deplo%:
#{ [[ "$*" == "y" ]] && ENVIRONMENT=$(USER); } || \
{ [[ "$*" =~ y-(.*) ]] && ENVIRONMENT=$${BASH_REMATCH[1]}; } || \
{ echo "$#: unknown target" && exit 1; }; \
echo "$#: ENVIRONMENT = $$ENVIRONMENT" && \
<do-whatever-else-is-needed>
$ USER=bar make deploy
deploy: ENVIRONMENT = bar
$ make deploy-foo
deploy-foo: ENVIRONMENT = foo
$ make deplorable
deplorable: unknown target
make: *** [Makefile:2: deplorable] Error 1
Do not forget to escape the recipe expansion by make ($$ENVIRONMENT).

Due to conversations w/#RenaudPacalet, I have learned that my approach mainly works because the variables defined in the deploy-% rules aren't used anywhere but the recipe...where they expand late, just before being passed to the shell. This lets me use $* in the variable definition because the variable definition wont be expanded until the second phase, when $* actually has a value.
The method for setting ENV_TYPE uses a trick with patsubst to produce the condition for an if by stripping the word "production" from the content of $ENVIRONMENT; in this context, an empty string selects the else case. So if $ENVIRONMENT is exactly equal to "production" then patsubst makes an empty string and the if evaluates to production, otherwise it evaluates to staging.
There's an explicit rule at the bottom for deploy- because that target would otherwise invoke some crazy implicit pattern rules that tried to compile deploy-.o
Finding that made me also consider the other error cases that could arise, so the first few lines define a function to ensure that, if a user specifies both ENVIRONMENT=X and uses a suffix Y, that there is an appropriate error message (rather than just having the suffix win). You can see the call to that function as the first line of the deploy-% recipe. There is another potential issue if $ENVIRONMENT is defined to have multiple words; the second deploy: line implements a test that will error out in this case---it tests that the word-count in $ENVIRONMENT is exactly 1 using the same patsubst/if trick as above.
It should also be noted that this makefile assumes the real work will be implemented in the recipe under deploy-%.
# define a function to detect mismatch between ENV and suffix
ORIG_ENV := $(ENVIRONMENT)
ENV_CHECK = $(if $(ORIG_ENV),$(if $(subst $(ORIG_ENV),,$(ENVIRONMENT)),\
$(error $$ENVIRONMENT differs from deploy-<suffix>.),),)
ENVIRONMENT ?= $(USER)
.PHONY: deploy
deploy: deploy-$(ENVIRONMENT)
deploy: $(if $(patsubst 1%,%,$(words $(ENVIRONMENT))),$(error Bad $$ENVIRONMENT: "$(ENVIRONMENT)"),)
.PHONY: deploy-%
deploy-%: ENVIRONMENT=$*
deploy-%: ENV_TYPE=$(if $(patsubst production%,%,$(ENVIRONMENT)),staging,production)
deploy-%:
$(call ENV_CHECK)
#echo ENVIRONMENT: $(ENVIRONMENT)
#echo ENV_TYPE: $(ENV_TYPE)
# keep it from going haywire if user specifies:
# ENVIRONMENT= make deploy
# or
# make deploy-
.PHONY: deploy-
deploy-:
$(error Cannot build with empty $$ENVIRONMENT)
Gives
$ USER=anonymous make deploy
ENVIRONMENT: anonymous
ENV_TYPE: staging
$ ENVIRONMENT=production make deploy
ENVIRONMENT: production
ENV_TYPE: production
$ ENVIRONMENT=test make deploy
ENVIRONMENT: test
ENV_TYPE: staging
$ make deploy-foo
ENVIRONMENT: foo
ENV_TYPE: staging
$ make deploy-production
ENVIRONMENT: production
ENV_TYPE: production
$ ENVIRONMENT=foo make deploy-production
Makefile:14: *** $ENVIRONMENT differs from deploy-<suffix>.. Stop.
$ ENVIRONMENT= make deploy
Makefile:24: *** Bad $ENVIRONMENT: "". Stop.
$ make deploy-
Makefile:24: *** Cannot build with empty $ENVIRONMENT. Stop.
$ ENVIRONMENT="the more the merrier" make deploy
Makefile:10: *** Bad $ENVIRONMENT: "the more the merrier". Stop.
Reflecting on how this works, it's not simple at all. There are various interpretations of $ENVIRONMENT...for example in the line deploy: deploy-$(ENVIRONMENT), that sense of $ENVIRONMENT gets the one that comes in from the shell's environment (possibly being set to $(USER) if absent). There's another sense in the recipe line #echo ENVIRONMENT: $(ENVIRONMENT) which will be the one assigned in deploy-%: ENVIRONMENT=$* just above, but after expansion. I am struck by the analogy with scoping or shadowing of variables in programming.

Related

Using different list elements for each usage of implicit recipe

There was a regular Makefile. Now I'm trying to enhance it to execute implicit recipe on different hosts via ssh. I have to do that using regular make/gmake as it's prohibited to install any 3rd-party packages on machines in that network.
Here is the concept:
SHELL := bash
SOURCES = $(wildcard *.in)
TARGETS = $(SOURCES:.in=.out)
TARGET_HOSTS := localhost foo bar
ONE_OF_TARGET_HOSTS := localhost
ifeq (${PROCESS_MODE},)
PROCESS_MODE := LOCAL
endif
all: $(TARGETS)
ifeq (${PROCESS_MODE},LOCAL)
%.out: %.in
echo "Running $# on host $(shell hostname)"
echo $* > $#
# some other stuff to run
else ifeq (${PROCESS_MODE},SSH)
DONT_PASS_ENVIRONMENT_VARIABLES := BASH.*|DISPLAY|EC_.+|HOST|HOSTNAME|MACHTYPE|OSTYPE|PROCESS_MODE|PPID|PWD|SHELL|SHLVL|SHELLOPTS|SSH_.+|TZ
SHELL_ENV := $(shell export -p | cut -b 12- | grep = | grep -Ev "^($(DONT_PASS_ENVIRONMENT_VARIABLES))")
%.out: %.in
echo "Pushing job $# on remote host $(ONE_OF_TARGET_HOSTS)"
#ssh $(ONE_OF_TARGET_HOSTS) 'cd "$(shell pwd)" && env $(SHELL_ENV) $(MAKE) PROCESS_MODE=LOCAL $#'
endif
It works in general. By default it executes everything locally (as it did before). If I run make PROCESS_MODE=SSH it executes rule for each .in file via ssh.
The problem is - right now it temporarily uses the same host (which is stored in ONE_OF_TARGET_HOSTS variable) for all ssh spawns. But I need to run each instance of rule on different hosts, which are defined in TARGET_HOSTS variable.
Let's say there are several files: a.in, b.in, c.in, d.in, e.in, etc. I want them to be processed as:
`a.in` - on localhost,
`b.in` - on foo,
`c.in` - on bar,
`d.in` - on localhost,
`e.in` - on foo,
and so on...
(the actual files order/assignment does not matter actually; they simply should be different and use all hosts evenly)
Is that possible? Or maybe is there any other way to achieve that?
In the following GNU make solution we first build a balanced list of [name.out]host tokens from the list of name.out targets. You can use any strings X and Y instead of [ and ], as long as for any name.out, Xname.outY is not a prefix of another token. Adapt to your situation.
In the recipe of %.out we recover the corresponding token with $(filter [$#]%,$(TOKEN_LIST)) and extract the hostname part with $(patsubst [$#]%,%,...). We assign it to shell variable host and use this variable in the echo and ssh commands.
There are 2 important aspects to remember:
make expands the recipe before passing it to the shell. So, when using the value of a shell variable we must write $$host instead of $host. After the make expansion it will become $host, what we want to pass to the shell, and no just ost.
Each line of a recipe is executed by a different shell. In order to use a shell variable in several recipe lines we must join them together with ; (or &&, as you wish) such that they become one single line, executed by one single shell. But for better readability we can use the line continuation (with a trailing \).
TOO_LONG_HOST_LIST := $(foreach t,$(TARGETS),$(TARGET_HOSTS))
HOST_LIST := $(wordlist 1,$(words $(TARGETS)),$(TOO_LONG_HOST_LIST))
TOKEN_LIST := $(join $(patsubst %,[%],$(TARGETS)),$(HOST_LIST))
# ...
%.out: %.in
#host=$(patsubst [$#]%,%,$(filter [$#]%,$(TOKEN_LIST))); \
echo "Pushing job $# on remote host $$host"; \
ssh $$host 'cd "$(CURDIR)" && env $(SHELL_ENV) $(MAKE) PROCESS_MODE=LOCAL $#'
Demo:
$ touch a.in b.in c.in d.in e.in f.in g.in
$ make TARGETS="a.out b.out c.out d.out e.out f.out g.out" TARGET_HOSTS="1 2 3"
Pushing job a.out on remote host 1
Pushing job b.out on remote host 2
Pushing job c.out on remote host 3
Pushing job d.out on remote host 1
Pushing job e.out on remote host 2
Pushing job f.out on remote host 3
Pushing job g.out on remote host 1
Note: remember that the $$ and the line continuations (the trailing ; \) are essential to guarantee the proper expansion of the shell variable host and its availability in all lines of the recipe.
Note: using the shell make function in a recipe, which is already a shell script, is almost always wrong; I replaced $(shell pwd) by the GNU make variable $(CURDIR).
Meanwhile I came out to this solution:
COUNT_TARGETS := $(words $(TARGETS))
# repeat hosts in the list to have there enough hosts for all targets
define expand-hosts
$(eval COUNT_TARGET_HOSTS := $(words $(TARGET_HOSTS)))
$(if $(filter $(shell test $(COUNT_TARGET_HOSTS) -lt $(COUNT_TARGETS) && echo $$?),0), \
$(eval TARGET_HOSTS += $(TARGET_HOSTS)) \
$(call expand-hosts) \
)
endef
$(eval $(call expand-hosts))
# assign host to all targets
assign-hosts = $(1): SSH_TARGET_HOST = $(2)
$(foreach i, $(shell seq $(words $(TARGETS))), \
$(eval $(call assign-hosts,$(word $(i), $(TARGETS)),$(word $(i), $(TARGET_HOSTS)))) \
)
...
%.out: %.in
#echo "Pushing job $# on remote host $(SSH_TARGET_HOST)"
#ssh $(SSH_TARGET_HOST) 'cd "$(shell pwd)" && env $(SHELL_ENV) $(MAKE) -j1 PROCESS_MODE=LOCAL $#'
This way allows me to have all weird stuff outside the recipe.
But I will likely adopt the way of hosts list expansion from #RenaudPacalet answer.

Is it possible to use positional, rather than named, parameters in a Makefile?

I have created a working target in my Makefile, called test-path-testname:
## Runs tests that match a file pattern and test name to speed up test time
test-path-testname: ensure-env
docker-compose run -p 9229:9229 \
-e TYPEORM_URL=postgres://postgres#postgres/someapp_test \
-e DATABASE_URL_READONLY=postgres://postgres#postgres/someapp_test \
server npm run test-all -t $(path) -- \
--detectOpenHandles --watchAll --verbose=false -t "$(testname)"
.PHONY: test-path-testname
It functions perfectly, using the path and testname parameters:
make path=usersArea testname="should create a new user" test-path-testname
However that command quite long - is there way I can use positional, rather than named, parameters in a Makefile?
For example, I'd like to be able to run the above with:
make usersArea "should create a new user" test-path-testname
No it is not possible because all non-options that do not contain a = are treated as targets.
Edit after your comment with the motivation explained:
You are solving an XY problem. Instead of more variables, pick apart the target name $# with substitutions:
test-path-testname:
#echo path=$(word 2,$(subst -, ,$#)) testname=$(word 3,$(subst -, ,$#))
docker-compose ... -t $(word 3,$(subst -, ,$#)) ...
This assumes there are exactly two hyphens in the target name.

How to load and export variables from an .env file in Makefile?

What is the best way to use a .env in a Makefile, i.e. loading that file and exporting all variables for subshells in make?
It would be great if the proposed solution would work with make only, e.g. not using any third party tools. Also .env files support multiline variables like:
FOO="this\nis\na\nmultiline\nvar"
this is why this solution is probably not adequate.
Make does not offer any way to read a content of the file to some variable. So, I consider it impossible to achieve the result without using external tools. However, if I am wrong, I'd be glad to learn some new trick.
So, let's assume there are two files, .env, being a technically correct shell file:
FOO=bar
BAR="notfoo" # comment
#comment
MULTILINE="This\nis\nSparta!"
# comment
and script.sh:
#!/bin/bash
echo FOO=${FOO}
echo BAR=${BAR}
echo -e ${MULTILINE}
One solution is to include the .env file, then make sure variables are exported:
include .env
$(eval export $(shell sed -ne 's/ *#.*$$//; /./ s/=.*$$// p' .env))
all:
./script.sh
Because of different treatment of quotes by shell and make, you will see the quotes in output.
You can avoid that by reprocessing the variables by make:
include .env
VARS:=$(shell sed -ne 's/ *\#.*$$//; /./ s/=.*$$// p' .env )
$(foreach v,$(VARS),$(eval $(shell echo export $(v)="$($(v))")))
all:
./script.sh
but then the multiline variable will become a one-liner.
Finally, you can generate a temporary file to be processed by bash and source it before any command is run:
SHELL=bash
all: .env-export
. .env-export && ./script.sh
.env-export: .env
sed -ne '/^export / {p;d}; /.*=/ s/^/export / p' .env > .env-export
Oh, new lines got messed in this case in multiline variable. You need to additionally quote them.
Finally, you can add export to .env using above sed command, and do:
SHELL=bash
%: .env-export
. .env-export && make -f secondary "$#"
Found this and it worked great:
at top of makefile
ifneq (,$(wildcard ./.env))
include .env
export
endif
Then you have make variables for all your env, for example MY_VAR use as $(MY_VAR)
You can load specific .env file for each target by creating a function and target to use it with other targets when necessary. Here as an sample:
define setup_env
$(eval ENV_FILE := $(1).env)
#echo " - setup env $(ENV_FILE)"
$(eval include $(1).env)
$(eval export)
endef
devEnv:
$(call setup_env, dev)
prodEnv:
$(call setup_env, prod)
clean:
rm -rf bin/
build: clean
GOOS=linux GOARCH=amd64 go build -o bin/ ./cmd/...
dev: build devEnv
cd cmd/api && ./../../bin/api
migrate-dev-db: devEnv
sh +x database/migration/migrate.sh dev
migrate-prod-db: prodEnv
sh +x database/migration/migrate.sh
deploy: prodEnv
sh +x script/deployment/production/ec2-deploy.sh

gnu make - recipe to keep installed version of file aligned with a master version of file

So here's a Makefile to install foo.conf, based on a master copy called foo.conf.master. It installs it to the current directory rather than /etc, just for testing purposes:
all: foo.conf.copied
foo.conf.copied: foo.conf.master foo.conf
cp foo.conf.master foo.conf
touch $#
# Recipe to tell make that it is okay for foo.conf not to exist beforehand.
foo.conf:
So then create foo.conf.master:
$ touch foo.conf.master
$
and you're ready to test:
$ make
cp foo.conf.master foo.conf
touch foo.conf.copied
$
The point is that if I (with my "trusted" sysadmin hat on) modify foo.conf.master then make (possibly called by cron) will roll out the update:
$ touch foo.conf.master
$ make
cp foo.conf.master foo.conf
touch foo.conf.copied
$
But equally important: if I (with my "rogue" sysadmin hat on) modify the installed version then make will back out the update:
$ touch foo.conf
$ make
cp foo.conf.master foo.conf
touch foo.conf.copied
$
Woohoo.
Okay, so now the problem: obviously foo.conf isn't the only file I want do this for, so I need to change my static rules to pattern rules. Okay, that's easy: substitute foo.conf for % in targets and dependencies, substitute foo.conf for $* in the commands, and make a minor modification to the last recipe (which would otherwise become only '%:') so that it doesn't look like I'm trying to cancel a builtin pattern rule.
So clean up and create this Makefile:
all: foo.conf.copied
%.copied: %.master %
cp $*.master $*
touch $#
# Recipe to tell make that it is okay for foo.conf not to exist beforehand.
# Nop tells make that I'm not *cancelling* a pattern rule here
# (see http://stackoverflow.com/questions/34315150/make-implicit-rules-dont-work-without-command).
%: ;
But this doesn't work:
$ make
make: *** No rule to make target `foo.conf.copied', needed by `all'. Stop.
$
The error message is misleading; it is really foo.conf that it doesn't know how to make, which can be demonstrated by adding the following at the bottom of the Makefile:
foo.conf:
touch $#
But then that's a static rule again, which I don't want.
There are a couple more requirements I would also like to satisfy, which the above example doesn't demonstrate. These are:
foo.conf should be installable anywhere in the filesystem (e.g. /etc/foo/server/foo.conf)
foo.conf.master should be in a central directory, or subdirectly thereof, for all master versions, preferably without the '.master' extension (e.g. ~/poor-mans-puppet/master-files/etc/foo/foo.conf)
foo.conf.copied should be in a central directory, not in the same directory as foo.conf (e.g. ~/poor-mans-puppet/timestamp-files/etc/foo/foo.conf)
After much googling, hair pulling, I'm asking here! Any ideas please? (PS: if copying Makefiles from here, remember to change indentation back to tabs.)
Mad Scientist below suggested an elegant static rule, but I really need it to be a pattern rule. The reason is that I need to hook extra dependencies in using rules:
all: <new-dependency>
rather than hooking them in using variables:
STUFF_ALL_SHOULD_DEPEND_ON += <new-dependency>
The reason for this requirement is for consistency with how other (non-%.copied) targets are handled in my very large Makefile.
However, based on Mad Scientist's idea, I tried the following, which didn't work, but perhaps helps somebody to help me:
all: foo.conf.copied
%.copied: %.master %
$(eval FILES_FOR_WHICH_AN_EMPTY_RECIPE_ARE_NEEDED += $$*)
cp $*.master $*
touch $#
define GENERATE_STATIC_EMPTY_RULE
$(1):
endef
$(foreach X,$(FILES_FOR_WHICH_AN_EMPTY_RECIPE_ARE_NEEDED),$(eval $(call GENERATE_STATIC_EMPTY_RULE,$(X))))
Can you explain why you're using this extra ".copied" file? Why don't you just use:
%: %.master ; cp $< $#
?
Anyway, you're running afoul of make's special rules related to match-anything rules (pattern rules like % that can build everything). If you change your pattern so it's not match-anything, like %.conf: ; then it will work. However you probably don't want to assume that all files end in .conf.
Alternatively you can use static pattern rules, like this:
FILES_TO_COPY = foo.conf bar.conf biz.baz
all: $(FILES_TO_COPY:%=%.copied)
$(FILES_TO_COPY:%=%.copied): %.copied : %.master %
cp $*.master $*
touch $#
and you don't need the extra pattern rule.
In the end, I dynamically generated static rules. The following pseudo-code hopefully makes the actual Makefile easier to understand:
if flag not set # flag won't be set on first call
prepare static rules
set flag # prevent running this clause again
recurse! # make invokes make
else
include static rules
do the normal thing
endif
Here's the real Makefile:
ifeq ($(MAKELEVEL),0)
all:
for X in $(patsubst %.copied,%,$^); do \
echo "$$X.copied: $$X.master $$X"; \
echo " cp $$X.master $$X"; \
echo " touch \$$#"; \
echo "$$X: ;"; \
done > Makefile.include
$(MAKE)
# The real dependencies on all are defined below, but while creating
# Makefile.include, we don't want make to digress and start making
# the dependencies; this pattern rule will stop it from doing that.
%.copied: ;
else
include Makefile.include
endif
all: foo.conf.copied
The outer make can be silenced by use of .SILENT and the --no-print-directory option (not shown above for clarity).
Here's the output:
$ touch foo.conf.master
$ make
cp foo.conf.master foo.conf
touch foo.conf.copied
$ touch foo.conf
$ make
cp foo.conf.master foo.conf
touch foo.conf.copied
$

How do you conditionally call a target based on a target variable (Makefile)?

I want a different version of the clean target to run based on whether make dev or make prod are run on a makefile.
I'm not compiling anything per se, just want to conditionally call a particular target or set of targets based on a variable, for example:
ifeq ($(BUILD_ENV),"development")
clean: -clean
else
clean: -clean-info
endif
#---------------------------------
dev: BUILD_ENV = development
dev: dev-setup which-env
#---------------------------------
prod: BUILD_ENV = production
prod: prod-setup which-env
#---------------------------------
which-env: clean
#echo -e "$(GREEN)$(BUILD_ENV)!$(CLEAR)"
-clean: -clean-info -clean-logs | silent
#echo -e "$(GREEN)</CLEAN>$(CLEAR)"
-clean-info:
#echo -e "$(GREEN)<CLEAN>...$(CLEAR)"
-clean-logs:
#echo -e " $(GREY)Removing log and status files $(CLEAR)";
#if [ -d .stat ]; then rm -rf .stat; fi
#rm -f *.log || true
Is there a way to do this with Makefiles? I havent found anything yet that illustrates this use-case.
I'm not trying to specifically clean anything or build anything this is just an example of me trying to conditionally call a set of targets. The actual targets could be anything else.
It's not at all clear that what you're asking for is really what you want, but here goes:
all:
ifeq ($(BUILD_ENV),development)
all: clean-dev
else
all: clean-other
endif
clean-dev:
#echo running $#, doing something
clean-other:
#echo running $#, doing something else
If you run make BUILD_ENV=development, you'll get something; if you run make or make BUILD_ENV=production you'll get something else.

Resources