Write Makefile for small projects

Posted by lingo5 on Thu, 30 May 2019 19:03:07 +0200

This article takes one of my small projects as an example to illustrate the basic writing of Makefile files for GNU Make.Since I haven't read the GNU Make documentation carefully and don't know about the POSIX-compliant Makefile format, I may not be writing a Makefile very seriously, so don't hesitate to ask someone who is good at it.

My small projects

This project is based on C, but because I'm a literal programming enthusiast, the project documentation and code are interwoven in a source file with an extension of.orz.When building a project, I need to use my own written programming tools orez Extract the C code from the.orz file and compile it with gcc.

The project name is agn and its directory structure is as follows:

agn
├── orez-src   # Store.orz files
└── src        # Store C source extracted from.orz file

Extracting C Source from.orz File

First, look at the files in the agn/orez-src directory:

$ cd agn
$ tree orez-src
├── agn_arena.orz
├── agn_array.orz
├── agn_build.orz
├── agn_delaunay_mesh.orz
├── agn_hash_table.orz
├── agn_kd_tree.orz
├── agn_km.orz
├── agn_linear_algebra.orz
├── agn_list.orz
├── agn_point.orz
├── agn_points.orz
├── agn_pqueue.orz
├── agn_simplex.orz
├── agn_tree.orz
├── agn_types.orz
└── agn_vector.orz

Using agn_array.orz as an example, extract the C source file from it using the following command:

$ orez -t -e ang_array.h ang_array.orz
$ orez -t -e agn_array.c agn_array.orz

For each.Orz file, the corresponding C source file can be extracted in the above way, except for the agn_build.orz file, which does not contain the C source.

Here's how to write a Makefile for this in the agn/orez-src directory.

Fundamentals of Makefile

Makefile is essentially a tree, with each node called a "target" and each node's children called a "dependency".For example:

all: agn_arena.h agn_arena.c \
     agn_array.h agn_array.c \
     agn_hash_table.h agn_hash_table.c \
     ... ... ...

The first target in Makefile is the root node of the tree described by Makefile.

The dependency of the target all is to extract.h and.c files from all.Orz files except agn_build.orz above.Backslashes are line continuations.Without a line continuation, all dependencies can only be written on the same line.

Each dependency of all is itself a node of the tree described by Makefile, and they may also have child nodes (dependencies).For example:

agn_arena.h: agn_arena.orz
agn_arena.c: agn_arena.orz
... ... ...

Both agn_arena.h and agn_arena.c depend on agn_arena.orz.Similarly, agn_arena.orz has a dependency, but its dependency is somewhat special. It depends on me, because all of the.Orz is handwritten by me.Since I can't really put me in Makefile as a dependency on the.Orz file, the dependency chain ends up with the.Orz file, which forms the leaf node of the tree that Makefile describes.

My task is to extract.H and.C files from the.Orz file using Makefile, which needs to be performed by commands attached to the Makefile target.Makefile allows you to place a set of Shell commands below each target.For example, the commands to extract agn_array.h and agn_array.c from the agn_array.orz file can be placed below the agn_array.h and agn_array.c targets, respectively:

agn_arena.h: agn_arena.orz
    orez -t -e ang_array.h ang_array.orz
agn_arena.c: agn_arena.orz
    orez -t -e ang_array.c ang_array.orz

Indent each command with the Tab key before it.Remember, always use the Tab key, not the spaces!Be sure to use the Tab key, not the space!Be sure to use the Tab key, not the space!

Based on goals, dependencies, and commands, you can write a rule for Makefile.

Makefile written by patient people

With this basic knowledge, I can write a Makefile that can extract C source from a set of.orz files:

all: agn_arena.h agn_arena.c \
     agn_array.h agn_array.c \
     agn_delaunay_mesh.h agn_delaunay_mesh.c \
     agn_hash_table.h agn_hash_table.c \
     agn_kd_tree.h agn_kd_tree.c \
     agn_km.h agn_km.c \
     agn_linear_algebra.h agn_linear_algebra.c \
     agn_list.h agn_list.c \
     agn_point.h agn_point.c \
     agn_points.h agn_points.c \
     agn_pqueue.h agn_pqueue.c \
     agn_simplex.h agn_simplex.c \
     agn_tree.h agn_tree.c \
     agn_types.h agn_types.c \
     agn_vector.h agn_vector.c
agn_agn_arena.h: agn_agn_arena.orz
    orez -t -e agn_agn_arena.h agn_agn_arena.orz
agn_agn_arena.c: agn_agn_arena.orz
    orez -t -e agn_agn_arena.c agn_agn_arena.orz
agn_agn_array.h: agn_agn_array.orz
    orez -t -e agn_agn_array.h agn_agn_array.orz
agn_agn_array.c: agn_agn_array.orz
    orez -t -e agn_agn_array.c agn_agn_array.orz
agn_agn_delaunay_mesh.h: agn_agn_delaunay_mesh.orz
    orez -t -e agn_agn_delaunay_mesh.h agn_agn_delaunay_mesh.orz
agn_agn_delaunay_mesh.c: agn_agn_delaunay_mesh.orz
    orez -t -e agn_agn_delaunay_mesh.c agn_agn_delaunay_mesh.orz
agn_agn_hash_table.h: agn_agn_hash_table.orz
    orez -t -e agn_agn_hash_table.h agn_agn_hash_table.orz
agn_agn_hash_table.c: agn_agn_hash_table.orz
    orez -t -e agn_agn_hash_table.c agn_agn_hash_table.orz
agn_agn_kd_tree.h: agn_agn_kd_tree.orz
    orez -t -e agn_agn_kd_tree.h agn_agn_kd_tree.orz
agn_agn_kd_tree.c: agn_agn_kd_tree.orz
    orez -t -e agn_agn_kd_tree.c agn_agn_kd_tree.orz
agn_agn_km.h: agn_agn_km.orz
    orez -t -e agn_agn_km.h agn_agn_km.orz
agn_agn_km.c: agn_agn_km.orz
    orez -t -e agn_agn_km.c agn_agn_km.orz
agn_agn_linear_algebra.h: agn_agn_linear_algebra.orz
    orez -t -e agn_agn_linear_algebra.h agn_agn_linear_algebra.orz
agn_agn_linear_algebra.c: agn_agn_linear_algebra.orz
    orez -t -e agn_agn_linear_algebra.c agn_agn_linear_algebra.orz
agn_agn_list.h: agn_agn_list.orz
    orez -t -e agn_agn_list.h agn_agn_list.orz
agn_agn_list.c: agn_agn_list.orz
    orez -t -e agn_agn_list.c agn_agn_list.orz
agn_agn_point.h: agn_agn_point.orz
    orez -t -e agn_agn_point.h agn_agn_point.orz
agn_agn_point.c: agn_agn_point.orz
    orez -t -e agn_agn_point.c agn_agn_point.orz
agn_agn_points.h: agn_agn_points.orz
    orez -t -e agn_agn_points.h agn_agn_points.orz
agn_agn_points.c: agn_agn_points.orz
    orez -t -e agn_agn_points.c agn_agn_points.orz
agn_agn_pqueue.h: agn_agn_pqueue.orz
    orez -t -e agn_agn_pqueue.h agn_agn_pqueue.orz
agn_agn_pqueue.c: agn_agn_pqueue.orz
    orez -t -e agn_agn_pqueue.c agn_agn_pqueue.orz
agn_agn_simplex.h: agn_agn_simplex.orz
    orez -t -e agn_agn_simplex.h agn_agn_simplex.orz
agn_agn_simplex.c: agn_agn_simplex.orz
    orez -t -e agn_agn_simplex.c agn_agn_simplex.orz
agn_agn_tree.h: agn_agn_tree.orz
    orez -t -e agn_agn_tree.h agn_agn_tree.orz
agn_agn_tree.c: agn_agn_tree.orz
    orez -t -e agn_agn_tree.c agn_agn_tree.orz
agn_agn_types.h: agn_agn_types.orz
    orez -t -e agn_agn_types.h agn_agn_types.orz
agn_agn_types.c: agn_agn_types.orz
    orez -t -e agn_agn_types.c agn_agn_types.orz
agn_agn_vector.h: agn_agn_vector.orz
    orez -t -e agn_agn_vector.h agn_agn_vector.orz
agn_agn_vector.c: agn_agn_vector.orz
    orez -t -e agn_agn_vector.c agn_agn_vector.orz

A person, whether he is writing Makefile or not, may laugh when he sees this Makefile above. It must be the Makefile written by the world's worst programmer.It's bad, but it shows patience.

Place this Makefile in the agn/orez-src directory and execute it in that directory

$ make

The corresponding.h and.c files can be extracted from each.orz file.

Pattern Rules

In the very lengthy Makefile in the previous section, all.h and.c targets depend on their.orz, and their names are the same except for the suffix.Therefore, all.h and.c targets repeat the following pattern:

%.h: %.orz
    ...command...
%.c: %.orz
    ...command...

In Makefile, these forms of goals and dependencies are called pattern rules.With pattern rules, you can replace all.h and.c target rules with the two rules above.

Although pattern rules can greatly simplify Makefile, they create a new problem. How do you write commands in pattern rules?In general rules, both the name of the target and the name of the dependency are determined, and the command from which the dependency produces the target is also determined, for example:

agn_array.h: agn_array.orz
    orez -t -e ang_array.h ang_array.orz

If you rewrite it as a pattern rule, is it like the following?

%.h: %.orz
    orez -t -e %.h %.orz

That's wrong!

Commands in pattern rules need to use the symbols specified in Makefile when using target and dependent names.The symbol for the target name is $@.Dependencies may be a list, $<symbols refer to the first dependency in the dependency list.These two symbols can also be used in general rules.Remember them!

The pattern rule for the above error should be changed to:

%.h: %.orz
    orez -t -e $@ $<

Based on the above knowledge, the complete contents of a greatly simplified Makefile are now given:

all: agn_arena.h agn_arena.c \
     agn_array.h agn_array.c \
     agn_delaunay_mesh.h agn_delaunay_mesh.c \
     agn_hash_table.h agn_hash_table.c \
     agn_kd_tree.h agn_kd_tree.c \
     agn_km.h agn_km.c \
     agn_linear_algebra.h agn_linear_algebra.c \
     agn_list.h agn_list.c \
     agn_point.h agn_point.c \
     agn_points.h agn_points.c \
     agn_pqueue.h agn_pqueue.c \
     agn_simplex.h agn_simplex.c \
     agn_tree.h agn_tree.c \
     agn_types.h agn_types.c \
     agn_vector.h agn_vector.c

%.h: %.orz
    orez -t -e $@ $<
%.c: %.orz
    orez -t -e $@ $<

Continue simplification

The Makefile given at the end of the previous section still looks uncomfortable.Since so many.h and.c target schema rule statements can be simplified to two lines at a time, what makes all targets so dependent?In addition, there is a problem that all dependencies on all targets are hard, and when adding or deleting.orz files to the orez-src directory, the dependency list for all targets needs to be maintained manually.These two questions can be boiled down to one question, how do I automatically generate this dependency list?To solve this problem, there is only one method, that is, to find the law.With regularity, automation can be done.

All dependencies of the all target are.h and.c files extracted from the.orz file, and these.h and.c file names differ only by extension from the.orz file name.These dependencies can be easily generated using this Bash script:

#!/bin/bash
echo -n "all: "
for i in $(ls *.orz)
do
    if [ "$i" = "agn_build.orz" ]
    then
    continue
    fi
    target=${i%%.orz}
    echo -n " ${target}.h ${target}.c"
done
echo ""

The principle is to use the ls command to get all files in the current directory with an extension of.Orz. The rest is a string substitution trick that removes the.Orz suffix from the file name of.Orz and then suffixes it with.h and.c.Note that as mentioned earlier, there is no need to generate.h and.c files from agn_build.orz, so the above Bash code ignores agn_build.orz when generating dependency lists.

Makefile can do this little trick, and it's much simpler.Makefile can use the built-in function wildcard provided by the Make tool (GNU Make), which functions like ls *.orz in the Bash code above, except that it returns the result to a Makefile variable.For example:

orz_files = $(wildcard *.orz)

$(wildcard *.orz) is the call to the wildcard function, and *.orz is the parameter passed to the function.The result returned by wildcard is saved in the variable orz_files.

The above Makefile statement does not have the ability to exclude agn_build.orz because the $(wildcard *.orz) returns all.Orz file names in the directory where Makefile resides, so agn_build.orz is included.Agn_build.orz can be excluded from the result returned by $(wildcard *.orz) to solve this problem, but this requires calling the Make built-in function subst:

orz_files = $(subst agn_build.orz,,$(wildcard *.orz))

subst is a multi-parameter function with intervals between parameters and no spaces between parameters.This function replaces part of a string (the first parameter) in a string (the third parameter) with the specified string (the second parameter).The Makefile statement above can be interpreted as deleting (replacing with an empty string) agn_build.orz from the string returned by $(wildcard *.orz).

Now that you can get all the.Orz file names in the directory where Makefile resides except agn_build.orz and present them in a variable, the next problem to solve is to generate.h and.c file names based on these.Orz file names.This problem can continue to be solved with the subst function:

c_head_files = $(subst .orz,.h,$(orz_files))
c_files = $(subst .orz,.c,$(orz_files))

These two lines replace.Orz with.h and.c in the orz_files variable, respectively.Note that using a variable in Makefile is similar to calling a function without parameters.

Makefile provides a more concise equivalent if it simply replaces characters in a variable:

c_head_files = $(orz_files:.orz=.h)
c_files = $(orz_files:.orz=.h)

With this knowledge, you can write a very concise Makefile that can generate C source files from all.Orz files except agn_build.orz:

orz_files = $(subst agn_build.orz,,$(wildcard *.orz))
c_head_files = $(orz_files:.orz=.h)
c_files = $(orz_files:.orz=.h)

all: $(c_head_files) $(c_files)

%.h: %.orz
    orez -t -e $@ $<

%.c: %.orz
    orez -t -e $@ $<

Add some commands

The.h and.c files generated by.orz need to be moved to the agn/src directory, and two file copy commands are added to the pattern rule to solve this problem:

%.h: %.orz
    orez -t -e $@ $<
    cp $@ ../src

%.c: %.orz
    orez -t -e $@ $<
    cp $@ ../src

Indent each command with the Tab key before it.Remember, always use the Tab key, not the spaces!Be sure to use the Tab key, not the space!Be sure to use the Tab key, not the space!

If you want to delete all.h and.c files generated by.orz, you can add another rule:

clean:
    rm -rf $(c_head_files) $(c_files)

In the directory where Makefile is located, the above rules for deleting.h and.c files work by executing the following commands:

$ make clean

The complete content of Makefile, which adds the above functionality, is as follows:

orz_files = $(subst agn_build.orz,,$(wildcard *.orz))
c_head_files = $(orz_files:.orz=.h)
c_files = $(orz_files:.orz=.h)

all: $(c_head_files) $(c_files)

%.h: %.orz
    orez -t -e $@ $<
    cp $@ ../src

%.c: %.orz
    orez -t -e $@ $<
    cp $@ ../src

clean:
    rm -rf $(c_head_files) $(c_files)

Target and dependency are files with a time state

To play a small game.Use Makefile from the previous section to generate.h and.c files, then add a file named clean to the directory where Makefile resides, and execute the make clean command, which is to execute the following commands in the agn/orez-src directory:

$ make
$ touch clean
$ make clean

What happens to the result?

The make clean command is invalid, it cannot delete.h and.c files that are generated from.orz files, and it will be prompted as follows:

make: 'clean' is up to date.

What happened?

The Make tool treats the clean target in Makefile as the newly created clean file.Since the clean target has no dependency on Makefile and the corresponding file for this target already exists, the Make tool assumes that nothing needs to be done for the clean target.

Now remember the first meaning of Makefile. Both the target and the dependency are files. Then remember the second meaning. If the target is newer than the dependency, Make refuses to execute the command in the rule where the target is located.

Look at the following pattern rules:

%.h: %.orz
    orez -t -e $@ $<
    cp $@ ../src

%.h and%.orz correspond to files in the same directory as Makefile. If%.h is newer than%.orz, none of the commands in this rule will be executed by the Make tool.

Now, let's have a look at the start of that little game.Because the clean target in Makefile has the same name as the clean file in the same directory as Makefile, the Make tool assumes that the clean in Makefile corresponds to the actual clean file, and because the clean target has no dependency, this means that it is always up to date, so commands in this rule will not be executed by the Make tool.

To remove the association of the clean target in Makefile with a file named clean, the pseudo target of Makefile, namely.PHONY, is needed:

.PHONY: clean
clean:
    rm -rf $(c_head_files) $(c_files)

.PHONY is a built-in target for Makefile.When the Make tool encounters the target, it knows that the target's dependencies are neither files nor pseudo targets.

This complete Makefile below solves all the above problems.

orz_files = $(subst agn_build.orz,,$(wildcard *.orz))
c_head_files = $(orz_files:.orz=.h)
c_files = $(orz_files:.orz=.h)

all: $(c_head_files) $(c_files)

%.h: %.orz
    orez -t -e $@ $<
    cp $@ ../src

%.c: %.orz
    orez -t -e $@ $<
    cp $@ ../src

.PHONY: clean

clean:
    rm -rf $(c_head_files) $(c_files)

Compilation of C Source Files

Now you can go to the agn/src directory and write a Makefile to compile the.C and.h files and get the executable.To write this Makefile, the Makefile knowledge provided above is sufficient. What you really need is to know how to compile C programs with gcc and the standard directory structure for Linux systems (or Unix-like systems).Here's what this Makefile is all about:

CC = gcc
CFLAGS = -std=c11 -pedantic -Werror -O2 -fPIC -pipe -I./
LDFLAGS = -shared -lm -lgsl -lgslcblas -lqhull_r
INSTALL = /usr/bin/install -c

prefix=/usr/local
includedir=$(prefix)/include
libdir=$(prefix)/lib

c_files = $(wildcard *.c)
objects = $(c_files:.c=.o)

libagn.so: $(objects)
    $(CC) $(LDFLAGS) $^ -o libagn.so

%.o: %.c
    $(CC) $(CFLAGS) -c $^ -o $@

.PHONY: install uninstall clean

install:
    mkdir -p $(includedir)/agn
    cp $(LIB_HEADERS) $(includedir)/agn
    $(INSTALL) libagn.so $(libdir)

uninstall:
    rm -rf $(includedir)/agn
    rm -f $(libdir)/libagn.so

.PHONY: clean
clean:
    rm -f $(objects) libagn.so

Makefile in project top-level directory

Now, both subdirectories of the agn directory, orez-src and src, have a Makefile.To build an entire agn project, you can execute make once in the orez-src directory and then once in the SRC directory, but doing so is cumbersome.For convenience, you can write a Makefile in the agn directory to accomplish these tasks.

No more Makefile knowledge is required to write this Makefile, and that knowledge is sufficient.This Makefile is like a user interface:

agn_host_ip = 192.168.3.7

.PHONY: all install uninstall clean update-orez

all:
    cd orez-src; make
    cd src; make

install:
    cd src; make install

uninstall:
    cd src; make uninstall

clean:
    cd orez-src; make clean
    cd src; make clean

update-orez:
    rsync -av $(agn_host_ip)::agn-orez-files orez-src/

In addition to generating C source files from.orz files and compiling C source files, this Makefile also has the ability to synchronize the latest.orz files from a specified machine in the LAN using the rsync tool.

Concluding remarks

Many years ago, I basically mastered the use of GNU Autotools and wrote an incomplete guide to GNU Autotools, probably the best for beginners in China, but now I am writing Makefile in handwriting.There are no big projects, and for non-C projects, mastering GNU Autotools is similar to mastering dragon slaughtering.

Tools such as GNU Autotools and Make may be appropriate for larger C/C++ projects, but they are not.For example, Linux kernels and Android systems are built directly from Makefile.The real difficulty with large projects should be the big projects themselves, not the small things like handwritten Makefiles.

One day, when I find handwriting Makefile tedious, consider using GNU Autotools or CMake or other so-called more modern project building tools.

Topics: C Makefile Programming Linux rsync