Automatically generate dependencies


1. Defects caused by compilation behavior

  • The preprocessor inserts the code from the header file directly into the source file
  • The compiler generates object files only from preprocessed source files
  • Therefore, the command may not be executed because the rules depend on the source file

Example 1 Check whether the following makefile file is correct: After modifying the contents of the macro HELLO in func.h, execute the make command and find that the compiler cannot update main.c and func.c, and therefore cannot update the result of execution: The reason is that the updated content in func.h cannot be automatically updated to the func.c and main.c files, resulting in no change in the compiled hello.out file.

func.h

#ifndef FUNC.H
#define FUNC.H

#define HELLO "hello makefile"

void foo();

#endif
Copy the code

func.c

#include "stdio.h"
#include "func.h"

void foo()
{
    printf("void foo():%s\n",HELLO);
    
}
Copy the code

main.c

#include "stdio.h"
#include "func.h"

extern void foo();

int main()
{
    foo();
    
    return 0;
}
Copy the code

makefile

OBJS := func.o main.o

hello.out := $(OBJS)
    @gcc -o $@ $^
    @echo "Target File => $@"
    
$(OBJS) : %.o : %.c
    @gcc -o $@ -c $^
Copy the code


Solution 1: Modify the Makefile file so that the header appears as a dependency condition in the rule corresponding to each target

OBJS := func.o main.o

hello.out := $(OBJS)
    @gcc -o $@ $^
    @echo "Target File => $@"
    
$(OBJS) : %.o : %.c func.h
    @gcc -o $@ -c $<
Copy the code

Advantages:

Changes to the header file are updated to the relevant source file and to the final target file

Disadvantages:

  • When header files are changed, any source files will be recompiled (compile inefficiently)
  • Makefiles can be difficult to maintain when the number of header files in a project is large

Solution 2:

  • Automatically generate a dependency on a header file by command
  • Automatically include the generated dependencies in the Makefile
  • Automatically identify the files that need to be recompiled when the header file is changed

Techniques needed for Solution 2:

(1) Linux sed command

  • Sed is a stream editor used to modify (add/delete/modify/look up) stream text.
  • Sed can be used for string substitution in stream text
  • Sed ‘s: SRC :des:g’

    For example, execute the following statement:

echo "test=>abc+abc+abc" | sed 's:abc:xyz:g'
Copy the code

The test content will change to xyz+xyz+xyz

Regular expression support for SED

  • In SED you can replace targets with regular expression matches
  • And you can use matching targets to generate replacement results for example
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
Copy the code

Example 2–sed usage

(2) compiler dependent generation option gcc-mm (gcc-m)

  • GCC -m des Obtains the complete dependency of the destination DES
  • Gcc-mm des; -e;

(3) Include keyword in makefile

  • This is similar to include in C
  • Move the contents of other files into the current file intact
  • Syntax: include filename For example: include foo.make include *.mk include $(var)

Make Specifies how to process the include keyword

Searches for target files in the current directory or specified directory

  • Search successful: File contents are moved into the current Makefile
  • Search failure: generates a warning – searches for and executes the rule targeting the filename – and eventually generates an error when the rule corresponding to the filename does not exist

Example 3-1–include usage — The target file does not exist, the target rule does not exist — No operation is performed, and an error message is reported

.PHONY : all

include test.txt

all :
    @echo "this is $@"
Copy the code

Example 3-2–include usage — If the target file does not exist, the target rule exists —- Find the rule by the file name and run the command

.PHONY : all

include test.txt

all :
    @echo "this is $@"
    
test.txt :
    @echo "this is $@"
Copy the code

Example 3-3–include usage — The target file exists, and the target rule exists

.PHONY : all

include test.txt

all :
    @echo "this is $@"
    
test.txt :
	@echo "creating $@"
	@echo "other : ; @echo "this is other"" > test.txt
Copy the code

— Executing the make command executes the first rule in the makefile


Include Dark operation 1:

  • Using the minus sign (-) not only turns off warnings from include, it also turns off errors; Make ignores errors when they occur!

Example 3-5-1–include not using (-) reports all errors and warnings

PHONY: all include test. TXT All: @echo "this is $@"
Copy the code

Example 3-5-2–include (-) turns off warnings and errors for include

PHONY: all include test. TXT All: @echo "this is $@"
Copy the code

Include Diablo 2:

  • If an include triggers a rule to create a file, the command in the rule is then executed, and the command after the include is re-executed

Example 3-6-1-include executes rules that do not have dependencies; The rule is included directly in the makefile

.PHONY : all

-include test.txt

all :
    @echo "this is $@"
    
test.txt :
    @echo "creating $@ ..."
    @echo "other"
Copy the code

Example 3-6-2–include checks to see if the rule exists, if it does, then checks if the dependency is up to date, and executes the dependency if it is newer than the current rule. Otherwise, execute the rule directly
The sample 3-6-2
makefile

.PHONY : all

-include test.txt

all :
    @echo "this is $@"
    
test.txt : b.txt
    @echo "creating $@ ..."
    @echo "all : c.txt" > test.txt
Copy the code

test.txt

all : a.txt
Copy the code

Example 3-6-2 If the rule file is newer than the dependent file –test. TXT is newer than the b.txt timestamp, run make all. The result is as follows:

Example 3-6-2 If the dependency file is newer than the rule file –b.txt is newer than the test. TXT timestamp, run make all:

Include’s summary:

  • If the target file does not exist —- find the rule with the file name and execute

  • If the target file does not exist and the found rule created the target file —- include the successfully created target file in the current makefile

  • If the target file exists, include the target file in the current makefile. And the target file name to find whether there is a corresponding rule — if so, compare the rule dependencies, decide whether to execute the command of the rule; Otherwise, no operation is performed.

  • When the target file exists and the rule corresponding to the target name is executed

    ifThe command in the rule updates the object file–make re-contains the target file, replacing the previously contained contents.

(4) The execution mechanism of commands in makefiles

  • Each command in the rule is executed in a new process by default (Shell)
  • You can use the continuation (;) Combine multiple commands into a single command
  • The combined commands are executed in sequence in the same process
  • Set -e specifies exit immediately after an error Example 4-1– execution of commands in rules — No continuation — Each command in rules is executed in a new process by default
.PHONY : all

all :
    mkdir test
    cd test
    mkdir subtest
Copy the code

Example 4-2- Execution of commands in a rule – continuation – Combined commands are executed in sequence in the same process

.PHONY : all

all :
    set -e; \ mkdirtest; \cd test; \ mkdir subtestCopy the code


A preliminary idea for Solution 2

  • Dep dependency files (partial dependencies of the target) obtained by gCC-MM and sed technical point: sequential execution of commands in rules
  • Include all. Dep dependency files via the include directive technical point: When. Dep dependency files do not exist, use the rule to automatically generate
  • When include finds that the. Dep file does not exist, create a deps file using the rule and command to create all. Dep files into the deps folder.
$(DIR_DEPS):
    $(MKDIR) $@

$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
    @echo "Creating $@ ..."
    @set -e; \ $(CC) -MM -E $(filter %.c,$^)) | sed's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
Copy the code

Looking at the code above,The (DIR_DEPS) %.c statement has a problem: the deps folder and the corresponding.dep file are created on the first execution, while the contents of the second deps folder are updated with a new.dep file, causing the first generated.dep file to be executed repeatedly because it depends on the update. The solution is as follows: use iFEQ to dynamically determine.

ifeq ("$(wildcard $(DIR_DEPS))"."")
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
Copy the code

Example 5 func. H

#ifndef FUNC_H
#define FUNC_H

#define HELLO "hello world"

void foo();

#endif
Copy the code

func.c

#include "stdio.h"
#include "func.h"

void foo()
{
	printf("void foo():%s\n",HELLO);	
}
Copy the code

main.c

#include "stdio.h"
#include "func.h"

extern void foo();

int main()
{
	foo();

	return 0;
}
Copy the code

makefile

.PHONY : all clean MKDIR := mkdir RM := rm -rf CC := gcc DIR_DEPS := deps SRCS := $(wildcard *.c) DEPS := $(SRCS:.c=.dep) DEPS :=  $(addprefix $(DIR_DEPS)/,$(DEPS)) ifeq ("$(MAKECMDGOALS)"."all")
-include $(DEPS)
endif

ifeq ("$(MAKECMDGOALS)"."")
-include $(DEPS)
endif

all :
    @echo "$@"
    
$(DIR_DEPS) :
    $(MKDIR) $@
    
ifeq ("$(wildcard $(DIR_DEPS))"."")
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif

    @echo "Creating $@ ..."
    @set -e; \ $(CC) -MM -E $(filter %.c,$^) | sed's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
    
clean :
    $(RM) $(DIR_DEPS)

Copy the code


A solution for automatically generating dependencies:

  • Add the dependency filename as the target to the automatically generated dependency
  • Determine whether to execute rules when loading dependent files through include
  • The dependency files are regenerated at rule execution time
  • Finally, load the new dependency file

Example 6– Final code implementation:

define.h

#ifndef DEFINE_H
#define DEFINE_H

#define HELLO "hello world"

#endif
Copy the code

func.h

#ifndef FUNC_H
#define FUNC_H

#include "define.h"

void foo();

#endif
Copy the code

func.c

#include "stdio.h"
#include "func.h"

void foo()
{
	printf("void foo():%s\n",HELLO);	
}
Copy the code

main.c

#include "stdio.h"
#include "func.h"

extern void foo();

int main()
{
	foo();

	return 0;
}
Copy the code

makefile

.PHONY : all clean

MKDIR := mkdir
RM := rm -rf
CC := gcc

DIR_DEPS := deps
DIR_EXES := exes
DIR_OBJS := objs

DIRS:= $(DIR_DEPS) $(DIR_EXES) $(DIR_OBJS)

EXE := app.out
EXE := $(addprefix $(DIR_EXES)/,$(EXE))

SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))



all : $(DIR_OBJS) $(DIR_EXES) $(EXE)

ifeq ("$(MAKECMDGOALS)"."all")
-include $(DEPS)
endif

ifeq ("$(MAKECMDGOALS)"."")
-include $(DEPS)
endif

$(EXE) : $(OBJS)
	$(CC) -o $@ $^
	@echo "Success! Target => $@"

$(DIR_OBJS)/%.o : %.c
	$(CC) -o $@ -c $(filter %.c,$^)	

$(DIRS) :
	$(MKDIR) $@

ifeq ("$(wildcard $(DIR_DEPS))"."")
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif

	@echo "Creating $@ ..."
	@set -e; \ $(CC) -MM -E $(filter %.c,$^) | sed's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@

clean :
	$(RM) $(DIRS)
Copy the code


Summary:

  • You can split the target’s dependencies into different places in the Makefile
  • The include keyword triggers the execution of the corresponding rule
  • If a rule is executed to a politically dependent update, it may cause the corresponding rule to be interpreted again
  • Dependent files also depend on the source files to make the correct compilation decisions
  • Automatically generating dependencies between files improves portability of Makefiles