makefile: pass variable number of arguments - makefile

I have a Makefile like so:
test:
pytest $(args)
.PHONY: test
My goal is to be able to call tests, like make test -v -k "test_foo", or make test -m
How do i accomplish this?

You can pass the arguments to the Makefile target as:
make args="-v -s" test

Related

How to get flags passed to a make target

I want to get the flags passed to a make target. Currently I'm able to get the words passed to it but I would need the flags also. This is the current code:
ifeq (docker,$(firstword $(MAKECMDGOALS)))
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
$(eval $(ARGS):;#:)
endif
.PHONY: docker
docker:
#${DOCKER_FILE} $(ARGS)
I want to run something liek this: make docker up -d
As written, your make invocation would treat up as a target and -d as a make parameter. You can't tell make to interpret the parameters differently. That being said, you can assign up -d to a variable and pass that in:
make docker ARGS="up -d"
.PHONY: docker
docker:
#${DOCKER_FILE} $(ARGS)
And this would do what you want. If you NEED to make the syntax similar to what you are describing for some reason, then you could create a wrapper script that would parse the arguments, and reformat them to a make specific target:
#!/bin/bash
[ "$1" == "docker" ] && make $1 ARGS="${#:2}"
bash> makedocker up -d

Makefile: Calling a recipe within another recipe will not run-dry it

Let's say I have a Makefile looking like this
file1:
$(MAKE) unittest
#echo "Making test file"
touch file1
unittest:
#echo Running test. In reality recipe will include many lines
.PHONY: unittest
My goal is to have a recipe to only run unit tests, but I also want to run them before creating the file (file1) and I don't want to run them if file1 exists.
This works when I run make, but I notice when I try
make file1 -n
file1 will be created and it is somehow due to having the make unittest call in the recipe.
Any clue about what is going on? Thanks in advance.
Either one of us is confused, or there's something about your makefile or environment you didn't share, or you have a buggy version of GNU make (what version do you have and what OS are you using? The version of GNU make that ships with MacOS is known to be old and Apple has made changes to it that have added bugs).
Doesn't reproduce on my system:
$ cat Makefile
file1:
$(MAKE) unittest
#echo "Making test file"
touch file1
unittest:
#echo Running test. In reality recipe will include many lines
.PHONY: unittest
$ ls file1
ls: cannot access 'file1': No such file or directory
$ make -n
make unittest
echo Running test. In reality recipe will include many lines
echo "Making test file"
touch file1
$ ls file1
ls: cannot access 'file1': No such file or directory
ETA
It's clear you don't want the file1 to be created if you run make -n.
But, your question didn't make clear whether you want the sub-make invocation of unittest to happen when you run make -n, or whether you don't want it to happen.
If you want unittest to be run but you don't want file1 to exist, but you do want to use .ONESHELL, then you're out of luck.
If you don't want unittest to run or file1 to exist when you run with -n, you can prevent it by fooling make into not noticing that the recipe is a recursive make invocation; all you have to do is not use the variable MAKE in the recipe. However, note that this has other side-effects particularly if you use -j to enable parallel builds:
NR_MAKE = $(MAKE)
file1:
$(NR_MAKE) unittest
#echo "Making test file"
touch file1

execute shell commands inside a target in makefile

I'm new to makefile. I'm trying to perform some shell operation inside a makefile under a target. I made a new_target without modifying the working code. The code looks like this:
all: new_target existing_target
new_target:
TEST_FILES:=$(wildcard $(HOME)/Test/*.cpp)
for f in $(TEST_FILES); do \
$(shell ls) $$f; \
done
Error:
TEST_FILES:=/docker_home/myhome/Test/b.cpp /docker_home/myhome/Test/file.cpp /docker_home/myhome/Test/a.cpp
/bin/sh: 1: TEST_FILES:=/docker_home/myhome/Test/b.cpp: not found
Makefile:6: recipe for target 'new_target' failed
make: *** [new_target] Error 127
The idea is to perform a shell operation(similar to ls) on all the .cpp files in a particular directory
This ...
TEST_FILES:=$(wildcard $(HOME)/Test/*.cpp)
... is (GNU) make syntax that assigns a value to a make variable. Your recipe instructs the shell to execute it as if it were a shell command. Obviously, that doesn't work.
Additionally, $(shell ls) doesn't do what you intend. It will run the ls command without arguments in make's working directory, at the time the makefile is parsed, and insert the results into the command to be run. If you want to run a shell command in your recipe then just put the command in the recipe.
The easiest solution would probably be to move that line outside the recipe (and dedent it):
TEST_FILES:=$(wildcard $(HOME)/Test/*.cpp)
new_target:
for f in $(TEST_FILES); do \
ls $$f; \
done
Note that the $(wildcard) function will be evaluated and the results assigned to TEST_FILES at the time that the makefile is parsed, not when the new_target target is built, but that appears unlikely to be an issue in this case.
Of course, unless you need TEST_FILES for something else, too, a much cleaner way would be to merge it together and get rid of wildcard:
new_target:
for f in $(HOME)/Test/*.cpp; do \
ls $$f; \
done
Or, best of all for this particular case:
new_target:
ls $(HOME)/Test/*.cpp
You need to run it in below way as TEST_FILES is a make variable and you should not mix make and shell:
TEST_FILES:=$(wildcard $(HOME)/Test/*.cpp)
new_target:
for f in $(TEST_FILES); do \
ls $$f; \
done
Note :
When it is time to execute recipes to update a target by make , they are executed by invoking a new sub-shell for each line of the recipe, unless the .ONESHELL special target is in effect. So you dont require a $(shell) explicitly.

Makefile - Execute a rule for every field in a variable

I have my project binary located at my repository's root, along with a Makefile used to build it.
This binary uses many of my self-made libraries, located in my lib/ folder
For the purpose of building (and cleaning) my repository's binary, I want to implement the following execution :
Instead of hardcoding the following lines,
clean_binaries:
make -C clean lib/folder1 -s
make -C clean lib/folder2 -s
make -C clean lib/another_folder -s
I created the BIN_PATH variable, containing the previous paths.
BIN_PATHS = lib/folder1 \
lib/folder2 \
lib/another_folder
And made a simple rule like this one :
clean_binaries: $(BIN_PATHS)
make -C clean $< -s
BUT it only executes the line for the first field of the variable (lib/folder1), which is not what I want to do.
I thought about using implicit rules(?), just like I compile the .c files, but I couldn't get it right.
In the end, I simply wonder how to execute a rule for every field of a given variable, and this inside a Makefile, if there is any way to do so.
Thank you for your answers :]
The way you get GNU make to generate a sequence of commands that vary by the
fields in a variable is to use the foreach function, e.g.
Makefile
BIN_PATHS := lib/folder1 lib/folder2 lib/another_folder
.PHONY: clean_binaries
clean_binaries:
$(foreach path,$(BIN_PATHS),make -C $(path) clean ;)
which runs like:
$ make
make -C lib/folder1 clean -s; make -C lib/folder2 clean -s; make -C lib/another_folder clean -s;
not requiring a shell-loop.
Note also that you need to correct:
make -C clean <directory>
to:
make -C <directory> clean

How to source a script in a Makefile?

Is there a better way to source a script, which sets env vars, from within a makefile?
FLAG ?= 0
ifeq ($(FLAG),0)
export FLAG=1
/bin/myshell -c '<source scripts here> ; $(MAKE) $#'
else
...targets...
endif
Makefile default shell is /bin/sh which does not implement source.
Changing shell to /bin/bash makes it possible:
# Makefile
SHELL := /bin/bash
rule:
source env.sh && YourCommand
To answer the question as asked: you can't.
The basic issue is that a child process can not alter the parent's environment. The shell gets around this by not forking a new process when source'ing, but just running those commands in the current incarnation of the shell. That works fine, but make is not /bin/sh (or whatever shell your script is for) and does not understand that language (aside from the bits they have in common).
Chris Dodd and Foo Bah have addressed one possible workaround, so I'll suggest another (assuming you are running GNU make): post-process the shell script into make compatible text and include the result:
shell-variable-setter.make: shell-varaible-setter.sh
postprocess.py #^
# ...
else
include shell-variable-setter.make
endif
messy details left as an exercise.
If your goal is to merely set environment variables for Make, why not keep it in Makefile syntax and use the include command?
include other_makefile
If you have to invoke the shell script, capture the result in a shell command:
JUST_DO_IT=$(shell source_script)
the shell command should run before the targets. However this won't set the environment variables.
If you want to set environment variables in the build, write a separate shell script that sources your environment variables and calls make. Then, in the makefile, have the targets call the new shell script.
For example, if your original makefile has target a, then you want to do something like this:
# mysetenv.sh
#!/bin/bash
. <script to source>
export FLAG=1
make "$#"
# Makefile
ifeq($(FLAG),0)
export FLAG=1
a:
./mysetenv.sh a
else
a:
.. do it
endif
Using GNU Make 3.81 I can source a shell script from make using:
rule:
<tab>source source_script.sh && build_files.sh
build_files.sh "gets" the environment variables exported by source_script.sh.
Note that using:
rule:
<tab>source source_script.sh
<tab>build_files.sh
will not work. Each line is ran in its own subshell.
This works for me. Substitute env.sh with the name of the file you want to source. It works by sourcing the file in bash and outputting the modified environment, after formatting it, to a file called makeenv which is then sourced by the makefile.
IGNORE := $(shell bash -c "source env.sh; env | sed 's/=/:=/' | sed 's/^/export /' > makeenv")
include makeenv
Some constructs are the same in the shell and in GNU Make.
var=1234
text="Some text"
You can alter your shell script to source the defines. They must all be simple name=value types.
Ie,
[script.sh]
. ./vars.sh
[Makefile]
include vars.sh
Then the shell script and the Makefile can share the same 'source' of information. I found this question because I was looking for a manifest of common syntax that can be used in Gnu Make and shell scripts (I don't care which shell).
Edit: Shells and make understand ${var}. This means you can concatenate, etc,
var="One string"
var=${var} "Second string"
I really like Foo Bah's answer where make calls the script, and the script calls back to make. To expand on that answer I did this:
# Makefile
.DEFAULT_GOAL := all
ifndef SOME_DIR
%:
<tab>. ./setenv.sh $(MAKE) $#
else
all:
<tab>...
clean:
<tab>...
endif
--
# setenv.sh
export SOME_DIR=$PWD/path/to/some/dir
if [ -n "$1" ]; then
# The first argument is set, call back into make.
$1 $2
fi
This has the added advantage of using $(MAKE) in case anyone is using a unique make program, and will also handle any rule specified on the command line, without having to duplicate the name of each rule in the case when SOME_DIR is not defined.
If you want to get the variables into the environment, so that they are passed to child processes, then you can use bash's set -a and set +a. The former means, "When I set a variable, set the corresponding environment variable too." So this works for me:
check:
bash -c "set -a && source .env.test && set +a && cargo test"
That will pass everything in .env.test on to cargo test as environment variables.
Note that this will let you pass an environment on to sub-commands, but it won't let you set Makefile variables (which are different things anyway). If you need the latter, you should try one of the other suggestions here.
My solution to this: (assuming you're have bash, the syntax for $# is different for tcsh for instance)
Have a script sourceThenExec.sh, as such:
#!/bin/bash
source whatever.sh
$#
Then, in your makefile, preface your targets with bash sourceThenExec.sh, for instance:
ExampleTarget:
bash sourceThenExec.sh gcc ExampleTarget.C
You can of course put something like STE=bash sourceThenExec.sh at the top of your makefile and shorten this:
ExampleTarget:
$(STE) gcc ExampleTarget.C
All of this works because sourceThenExec.sh opens a subshell, but then the commands are run in the same subshell.
The downside of this method is that the file gets sourced for each target, which may be undesirable.
Depending on your version of Make and enclosing shell, you can implement a nice solution via eval, cat, and chaining calls with &&:
ENVFILE=envfile
source-via-eval:
#echo "FOO: $${FOO}"
#echo "FOO=AMAZING!" > $(ENVFILE)
#eval `cat $(ENVFILE)` && echo "FOO: $${FOO}"
And a quick test:
> make source-via-eval
FOO:
FOO: AMAZING!
An elegant solution found here:
ifneq (,$(wildcard ./.env))
include .env
export
endif
If you need only a few known variables exporting in makefile can be an option, here is an example of what I am using.
$ grep ID /etc/os-release
ID=ubuntu
ID_LIKE=debian
$ cat Makefile
default: help rule/setup/lsb
source?=.
help:
-${MAKE} --version | head -n1
rule/setup/%:
echo ID=${#F}
rule/setup/lsb: /etc/os-release
${source} $< && export ID && ${MAKE} rule/setup/$${ID}
$ make
make --version | head -n1
GNU Make 3.81
. /etc/os-release && export ID && make rule/setup/${ID}
make[1]: Entering directory `/tmp'
echo ID=ubuntu
ID=ubuntu
--
http://rzr.online.fr/q/gnumake
Assuming GNU make, can be done using a submake. Assuming that the shell script that exports the variables is include.sh in the current directory, move your Makefile to realmake.mk. Create a new Makefile:
all:
#. ./include.sh; \
$(MAKE) -f realmake.mk $(MAKECMDGOALS)
$(MAKECMDGOALS):
+#. ./include.sh; \
$(MAKE) -f realmake.mk $(MAKECMDGOALS)
Pay attention to the ./ preceding include.sh.
Another possible way would be to create a sh script, for example run.sh, source the required scripts and call make inside the script.
#!/bin/sh
source script1
source script2 and so on
make
target: output_source
bash ShellScript_name.sh
try this it will work, the script is inside the current directory.

Resources