4
\$\begingroup\$

I am just experimenting with Bash and Makefiles to make my life easier, and here's what I came up with.

Scenario: Files are in a directory containing a tutorial, or an iteration. For example, one wants to experiment with Boost Libraries.

Code: There's two bashes and a Makefile. First the Makefile

Makefile

#PROJECT=
#DIRECTORY=
#PARAMETER=
OBJ=$(DIRECTORY)$(PROJECT).o
OUT=$(DIRECTORY)$(PROJECT)
CPP=$(DIRECTORY)$(PROJECT).cpp
CC=g++-7
DEBUG=-g
CFLAG=-Wall -c $(DEBUG)
LFLAG=-Wall $(DEBUG)
I=-I/user/local/boost
L=-lboost_system -lboost_thread -lpthread
STD=-std=c++1z

$(OBJ): $(CPP)
    $(CC) $(STD) $(CFLAG) $(CPP)

$(OUT): $(OBJ)
    $(CC) $(STD) $(LFLAG) $(OBJ) -o $(OUT) $(I) $(L)

clean:
    rm -rf $(OUT) || true
    rm -rf $(OBJ) || true

redo:
    make PROJECT=$(PROJECT) DIRECTORY=$(DIRECTORY) PARAMETER=$(PARAMETER) clean
    clear
    make PROJECT=$(PROJECT) DIRECTORY=$(DIRECTORY) PARAMETER=$(PARAMETER) $(OBJ)
    mv $(PROJECT).o $(DIRECTORY) || true
    make PROJECT=$(PROJECT) DIRECTORY=$(DIRECTORY) PARAMETER=$(PARAMETER) $(OUT)
    ./$(DIRECTORY)$(PROJECT) $(PARAMETER)

What this does is make the file with minimal editing allowing bash scripts to run the makes. Though I still need to work-out and externalize the L variable for bash scripts.

Here's m.sh. A better name could be chosen, but I was in a hurry. Perhaps bmake.sh.

#!/bin/bash

PROJECT=$1
DIRECTORY=$2
ACTION=$3
FLAG=$4
PARAMETER=$5

make PROJECT=$PROJECT DIRECTORY=$DIRECTORY PARAMETER=$PARAMETER $ACTION $FLAG

unset PROJECT
unset DIRECTORY
unset ACTION
unset FLAG
unset PARAMETER

This is the function that calls the Makefile. So instead of "make me some blah blah, it's "bash make.sh".

So, the test case is for this tutorial found here. It's a remarkable tutorial on network programming with Boost::Asio, and all of the examples are in a zip (1a.cpp, 1b.cpp, etc.). THe files are to be unzipped into a directory, which is hard coded in a bash, in this example "example/" is used. The scripts do what I wish. I suppose I have two scripts to make the distinction between the general and the specific.

#!/bin/bash

PROJECT=$1
DIRECTORY=example/ # or $2
ACTION=$3
FLAG=$4
PARAMETER=$5

bash m.sh $PROJECT $DIRECTORY $ACTION $FLAG $PARAMETER

This file is not strictly necessary, though, as you can simply execute m.sh. I simply created it for this test example. For FLAG, I recommend passing in -n, which outputs what the make file would've done if it had not been a test. This is very useful as it is dangerous to execute the makes cold out of the code freezer.

I thought I'd share and get some feedback on how to make it better as I am somewhat of a beginner on both bash scripting and Makefiles.

This is how to do a trial run:

bash example.sh 1a example redo -n

And, this is how to run it:

bash example.sh 1a example redo

Result:

make PROJECT=1a DIRECTORY=example/ PARAMETER= example/1a.o
make[1]: Entering directory '/wherever'
g++-7 -std=c++1z -Wall -c -g example/1a.cpp
make[1]: Leaving directory '/wherever'
mv 1a.o example/ || true
make PROJECT=1a DIRECTORY=example/ PARAMETER= example/1a
make[1]: Entering directory '/wherever'
g++-7 -std=c++1z -Wall -g example/1a.o -o example/1a -I/user/local/boost -lboost_system -lboost_thread -lpthread
make[1]: Leaving directory '/wherever'
./example/1a 
Do you reckon this line displays?

The redo allows it to clean itself from the last build, and do the build over again. It's important to force the directory so that you never do something stupid like delete a bunch of files.

Makefile (redo)

redo:
    make PROJECT=$(PROJECT) DIRECTORY=$(DIRECTORY) PARAMETER=$(PARAMETER) clean
    clear
    make PROJECT=$(PROJECT) DIRECTORY=$(DIRECTORY) PARAMETER=$(PARAMETER) $(OBJ)
    mv $(PROJECT).o $(DIRECTORY) || true
    make PROJECT=$(PROJECT) DIRECTORY=$(DIRECTORY) PARAMETER=$(PARAMETER) $(OUT)
    ./$(DIRECTORY)$(PROJECT) $(PARAMETER)

Though, instead of two bash files, I really only need one bash file with a configuration file, which the bash file can draw upon, which I think is the next logical step. Though it does suit my needs. Enjoy, and if so inclined, comment, and let me know how to best make a config file for this. Bash it if you must, haha.

\$\endgroup\$
  • \$\begingroup\$ Take a look at: Built Rules Makefile and example usage: Example Usage Makefile \$\endgroup\$ – Martin York Jun 16 '17 at 9:26
  • 1
    \$\begingroup\$ I think that CMake will make your life much easier. It is much easier to deal with(in my experience) and is cross platform. \$\endgroup\$ – Incomputable Jun 16 '17 at 23:08
6
\$\begingroup\$

The biggest problem I can see is that you are not using idiomatic Makefile names, which will confuse readers and frustrate users who want to change things via the command line. Typically CFLAGS is compiler flags passed to the C compiler, CPPFLAGS the preprocessor and CXXFLAGS the C++ compiler. $(CPP), $(I) and $(L) are are also just plain confusing and hard to read. I'd change $(CPP) to $(SOURCES), and $(I) to $(INCLUDES) AND $(L) to $(LDFLAGS). Please see the manual; the tutorial you are likely following (that I found via google) gives bad advice).

Another problem is that you put -c inside CFLAGS. This isn't a good place to put it as if someone wants to amend/change CFLAGS, then your Makefile will no longer work. You should have that line under a target/rule like this:

$(OBJ): $(CPP)
    $(CC) $(STD) -c $^ -o $@

You should always use the automatic variables here and not the actual variables.

\$\endgroup\$
3
\$\begingroup\$

Use Make built-in rules to compile source

The Makefile duplicates rules that are built into Make. Instead of writing

$(OBJ): $(CPP)
    $(CC) $(STD) $(CFLAG) $(CPP)

$(OUT): $(OBJ)
    $(CC) $(STD) $(LFLAG) $(OBJ) -o $(OUT) $(I) $(L)

You only need to write

default: $(OUT)
.PHONY: default

You will need to re-write to use the standard Make variables to take advantage of the built-in rules:

CXX := g++-7
# (not CC)

CXXFLAGS = $STD -Wall $(DEBUG) $I
# (not CFLAG)

LDLIBS = -lboost_system -lboost_thread -lpthread
# (not L)

If you ever forget what the built-in rules are, make --print-data-base is your friend.

Credit for including -Wall - that's a worthwhile flag and I never compile without it. Consider -Wextra too, if you're serious about improving your C and C++.

Clean and Redo targets

rm -f always succeeds, so no need to add || true after these commands. It's probably best to use the built-in $(RM), which will allow users to override the command if necessary (or to use a different built-in value on non-POSIX platforms such as VMS).

The redo target uses make - it's important if using a sub-make that you use $(MAKE) rather than make. That does the Right Thing in parallel builds and when just showing the commands (e.g. with make -n). And there's no need to explicitly pass Make variables to a sub-make - that happens automatically. The redo target is probably unnecessary anyway - you'll get the same effect much more simply by typing make -B.

Always specify delete-on-error

Make doesn't automatically remove a target if the command fails half-way through, unless you add this line:

 .DELETE_ON_ERROR:

Every makefile should include that - it's very rare you'd like a half-written file to be considered as completely made.


BTW, is that a typo, or do you really have Boost in /user/local/ instead of /usr/local/?

\$\endgroup\$
1
\$\begingroup\$

Sure:

#!/bin/bash

This does make it readable.

PROJECT=$1
DIRECTORY=$2
ACTION=$3
FLAG=$4
PARAMETER=$5

Here you should quote all your parameters.

make PROJECT=$PROJECT DIRECTORY=$DIRECTORY PARAMETER=$PARAMETER $ACTION $FLAG

If any of the parameters have a space (modern directories tend to have them). So best just to add double quotes around all parameters (I personally also like to add {} to make them stand out more but thats more optional).

make PROJECT="${PROJECT}" DIRECTORY="${DIRECTORY} PARAMETER="${PARAMETER}" "${ACTION}" "${FLAG}"

These are completely un-needed. The set above don't export them to the environment. So when this shell exits they don't exist anymore.

unset PROJECT
unset DIRECTORY
unset ACTION
unset FLAG
unset PARAMETER

In this line:

    mv $(PROJECT).o $(DIRECTORY) || true

The || just hides errors. You still want this to fail. Make has a special prefix that makes it ignore errors and continue. The following has the same affect:

    -mv $(PROJECT).o $(DIRECTORY)

Same applies to clean

clean:
        -rm -rf $(OUT)
        -rm -rf $(OBJ)

When you specify directories they normally don't include the trailling slash. So the following may not work as you expect:

OBJ=$(DIRECTORY)$(PROJECT).o
OUT=$(DIRECTORY)$(PROJECT)
CPP=$(DIRECTORY)$(PROJECT).cpp

Personally I think CPP should be SRC and OBJ should be generated from the set of SRC files.

SRC=$(wildcard $(DIRECTOR)/*.cpp)
OBJ=$(patsubst %.cpp, %.o, $(SRC))

Prefer the standard naming convention

CC is the C compiler. CXX is the C++ compiler

CC=g++-7

CFLAGS (note trailing S) is flags for the C compiler. CXXFLAGS are the flags for the C++ compiler.

CFLAG=-Wall -c $(DEBUG)

The linker flags should be in LDFLAGS (note trailing S)

LFLAG=-Wall $(DEBUG)

You can use += on variables in Make to incrementally add flags. This is better because a lot of people will set the standard flags on the command line before calling make

This is quite common

CXXFLAGS=-O3 make

Make also has a bunch of standard rules:

# Standard C++ rule
%.o: %.cpp
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $@ $<

So using this you can remove your custom rule.

So I would add the following definition

CXX       = $(CC)
CXXFLAGS += $(CFLAG) $(I) $(L) $(STD)

Then remove your rule for building objects.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.