I'm trying to compile my project with the Criterion framework for unit testing. Here's how my project is organized:
.
├── include
│ └── mysh.h
├── Makefile
├── obj
├── src
│ ├── arrays_handler.c
│ ├── builtin_env.c
│ ├── delete_from_env.c
│ ├── display_env.c
│ ├── display_prompt.c
│ ├── doubly_linked_list.c
│ ├── env_handler.c
│ ├── execute_command.c
│ ├── mysh.c
│ ├── my_strcmp.c
│ ├── my_strcpy.c
│ ├── my_strdup.c
│ ├── my_strlen.c
│ ├── my_strncmp.c
│ ├── my_strndup.c
│ ├── my_strrchr.c
│ ├── my_str_to_word_array.c
│ ├── name_slice.c
│ ├── parse_command.c
│ ├── sanitize.c
│ ├── shell_handler.c
│ └── strbind.c
└── tests
├── Makefile
└── sanitize_test.c
To compile my project, I use a first Makefile (the one at the root), which works very well. Here are its contents:
INCLUDE_DIR := include
OBJ_DIR := obj
SRC_DIR := src
NAME := mysh
CC := gcc
CFLAGS := -Wall -Wextra -Werror -ggdb3
CPPFLAGS := -I$(INCLUDE_DIR)
SRC_FILES := $(wildcard $(SRC_DIR)/*.c)
OBJ_FILES := $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC_FILES))
all: $(NAME)
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
@$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
$(NAME): $(OBJ_FILES)
@$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
clean:
@$(MAKE) -C ./tests tests_clean
@rm -rf $(OBJ_DIR)/*.o
fclean: clean
@$(MAKE) -C ./tests tests_fclean
@rm -rf $(NAME)
tests_run: fclean
@$(MAKE) -C ./tests tests_run
re: fclean all
.PHONY: all clean fclean tests_run re
The problem seems to lie with my second Makefile (in the test folder), which is unable to compile the tests_run
rule. Here are its contents:
CC := gcc
CFLAGS := -Wall -Wextra -Werror -ggdb3
CPPFLAGS := -I../include
LDFLAGS := -Lcriterion/lib
LDLIBS := -lcriterion
NAME = ../unit-tests
OBJ_DIR = ../obj
SRC = $(shell find ../src -name '*.c' ! -name 'mysh.c')
TEST_SRC = $(shell find . -name '*.c')
OBJ = $(patsubst ../src/%.c,$(OBJ_DIR)/%.o,$(SRC))
TEST_OBJ = $(patsubst ./%.c,$(OBJ_DIR)/%.o,$(TEST_SRC))
$(OBJ_DIR)/%.o: %.c
@mkdir -p $(OBJ_DIR)
@$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
tests_run: $(OBJ) $(TEST_OBJ)
@$(CC) $(CFLAGS) $(LDFLAGS) $(filter-out ../obj/mysh.o,$(TEST_SRC) $(SRC)) $(LDLIBS) -o $(NAME)
@./$(NAME)
tests_clean:
@rm -rf $(OBJ)
@rm -f ../*.gcda
@rm -f ../*.gcno
tests_fclean: tests_clean
@rm -f $(NAME)
.PHONY: tests_run tests_clean tests_fclean
I get the following error when calling tests_run
:
make[1]: enter the "[...]/tests" directory
make[1]: exit the "[...]/tests" directory
make[1]: enter the "[...]/tests" directory
make[1]: exit the "[...]/tests" directory
make[1]: enter the "[...]/tests" directory
make[1]: *** No rule to make target "../obj/display_prompt.o", required for "tests_run". Stop.
make[1]: exit directory "[...]/tests".
make: *** [Makefile:29: tests_run] Error 2
Can you help me correct this Makefile?
This error ...
make[1]: *** No rule to make target "../obj/display_prompt.o", required for "tests_run". Stop.
... is emitted when make
is processing your second makefile, but it is about ../obj/display_prompt.o
, which is the responsibility of your first makefile. One of the drawbacks of recursive make
, such as you are using, is that the separate make
runs are largely independent. There are ways for them to communicate, but they do not share rules or dependency information, so although the top-level makefile knows how to build obj/display_prompt.o
(relative to its own directory), the second level makefile does not know how build the same file, which for it is ../obj/display_prompt.o
.
The second-level makefile needs all the main object files, so if you want this recursive make
arrangement to work then the top level makefile needs to ensure that they are built before recursing to the second-level makefile. But it doesn't. In fact, it does the exact opposite. The top-level tests_run
target has fclean
as a prerequisite, which in turn has clean
as a prereq, which unconditionally deletes all the object files.
I see no reason at all for the top-level tests_run
target to delete the object files. It is counter-productive. Perhaps your idea is to perform a clean build of these files for the tests, but if your makefile correctly captures all their dependencies then that should not be necessary. Moreover, if ever you do want to perform a clean build before testing then you can request it easily enough: make fclean; make tests_run
.
Additionally, your second-level makefile should have a separate rule for building the unit-tests
executable. That build should not be buried in a recipe for running the same.
If you continue with the recursive make
approach, then instead of removing the object files, the top-level tests_run
rule must ensure, directly or indirectly, that they are built. That might look like this:
tests_run: $(OBJ_FILES)
$(MAKE) -C ./tests tests_run
I'm not a big fan of recursive make
, though I do use it in certain situations. Here, though, I would use a single makefile, non-recursively, to build everything. If you want, you can keep it somewhat modular by redesigning the second-level Makefile to be include
d by the top-level one, maybe something like this:
TESTS_LDFLAGS := -Ltests/criterion/lib
TESTS_LDLIBS := -lcriterion
TESTS_NAME = unit-tests
TESTS_SRC = $(shell find tests -name '*.c')
TESTS_OBJ = $(patsubst tests/%.c,$(OBJ_DIR)/%.o,$(TESTS_SRC))
$(TESTS_NAME): $(filter-out $(OBD_DIR)/mysh.o,$(OBJ)) $(TESTS_OBJ)
$(CC) $(CFLAGS) -o $@ $^ $(TESTS_LDFLAGS) $(TESTS_LDLIBS)
$(OBJ_DIR)/%.o: tests/%.c
mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
tests_run: $(TESTS_NAME)
./$(TESTS_NAME)
tests_clean:
rm -rf $(TEST_OBJ)
rm -f *.gcda
rm -f *.gcno
tests_fclean: tests_clean
rm -f $(TEST_NAME)
.PHONY: tests_run tests_clean tests_fclean
In the top-level makefile, you would want to remove run_tests
rule (tests_run
will instead be used directly), and add include tests Makefile
somewhere near the end. (Though if it were me, I would also rename the second-level file to tests.mk
or similar, to clarify that it is not (any longer) a standalone makefile.