textfiles

Various textfiles on technical topics
git clone https://git.bracken.jp/textfiles.git
Log | Files | Refs

commit 9c76e5c24386013c06a37e80444bf4fcc79a37bb
parent be4883e193957e3860b8f946c82f25188fda2c6d
Author: Chris Bracken <chris@bracken.jp>
Date:   Thu,  6 Jun 2024 13:03:53 -0700

Add A Template for Portable Idiomatic Makefiles

Diffstat:
Aidiomatic_makefiles.md | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 255 insertions(+), 0 deletions(-)

diff --git a/idiomatic_makefiles.md b/idiomatic_makefiles.md @@ -0,0 +1,255 @@ +# A Template for Portable Idiomatic Makefiles + +2022-04-07 [Original at seninha.org][original] + +In this post, I present the template I use to write portable, idiomatic +Makefiles for building C programs. + +I use `${MYVAR}`, with curly braces, rather than `$(MYVAR)`, with parentheses, +but both notations are supported. In this document I identify two different +people: the developer or Makefile author, who writes the Makefile; and the user +or package maintainer, who defines the proper variables and run `make(1)`. + +## The Project Files + +There are several files involved in the building process: the target files to be +built, the source files, and the intermediate files. They may be referenced +several times by different rules, so it is a good practice to name them with +variables. Suppose we're building a program called `myprog` composed of three +modules. These are the variables to be defined: + +```Makefile +PROG = myprog +SRCS = main.c parse.c util.c +OBJS = main.o parse.o util.o +``` + +The variable `PROG` is the final file; `SRCS` lists the source files; and `OBJS` +lists the intermediate, object files. Note that both the list of source files +and of object files are almost equal, differing only by the extension of the +files. POSIX `make(1)` has a notation for changing the ending of each word in a +variable. In order to avoid repeating ourselves, we can use this notation to +define `${OBJS}`. + +```Makefile +PROG = myprog +SRCS = main.c parse.c util.c +OBJS = ${SRCS:.c=.o} +``` + +We want to build our program when we call make without any arguments. To do +this, the first target should be `${PROG}` itself. However, it is a common +practice to use the target `all` to build the final files. So the first target +is `all`, which just has `${PROG}` as prerequisite. + +```Makefile +all: ${PROG} +``` + +Next we need to declare the dependencies between the program modules. This is +done with rules without commands. + +```Makefile +main.o: parse.h util.h +parse.o: parse.h util.h +util.o: util.h +``` + +## The Compilation Rules + +The compilation process is split in two parts: generate the object files from +the source files, and generate the program from the object files. So we need two +rules. + +The following rule builds object files (.o) from source files (.c). The `.c.o` +is an inference rule that declares each .c file to be the prerequisite of a +homonymous .o file. This notation is defined by POSIX and is, therefore, +portable (different from the `%.o`: `%.c` rule, which is a GNU extension). In +the command of an inference rule (and only in the command of an inference rule), +the `$<` variable evaluates to the prerequisite file. + +```Makefile +.c.o: + ${CC} -I/usr/X11R6/include ${CFLAGS} ${CPPFLAGS} -c $< +``` + +We use the variable `${CC}` to expand to the proper C compiler command. This +variable is set by default to the proper command. We should use this variable +rather than hardcoding it to `gcc`, for example. + +The variables `${CFLAGS}` and `${CPPFLAGS}` contains options that the user or +package maintainer wants to pass to the compiler or preprocessor. It is a bad +practice to define those variables in a Makefile; let the user (or package +maintainer) define them. Any option that must be passed to the compiler (such as +`-I/usr/X11R6/include` above) should be passed before those variables. If the +Makefile author, for example, define `${CFLAGS}` to `-I/usr/X11R6/include`, +either this value may override the values set by the user, or the values set by +the user may shadow the option set by the Makefile author. + +The following rule links all object files into the program. We define `${OBJS}` +to be the prerequisites of `${PROG}`. The `$@` variable evaluates to the target +file (`${PROG}` in our case). Since this is not an inference rule, the `$<` +variable cannot be used; we must write `${OBJS}` both in the rule and in the +command. + +```Makefile +${PROG}: ${OBJS} + ${CC} -o $@ ${OBJS} -L/usr/X11R6/lib -lX11 ${LDFLAGS} +``` + +The variable `${LDFLAGS}` contains options that the user or package maintainer +wants to pass to the linker. Again, it is a bad practice to define it in the +Makefile. The options `-L/usr/X11R6/lib` and `-lX11` are passed before this +variable (so the user can override or increment them if necessary). + +## The Installation rules + +Your Makefile may include rules for installing the final files in the system. In +this example, two files are installed, `${PROG}` (the final, compiled program), +and `${PROG}.1` (the manpage, named as the program followed by `.1`). The +following rule performs the installation. + +```Makefile +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + mkdir -p ${DESTDIR}${MANPREFIX}/man1 + install -m 755 ${PROG} ${DESTDIR}${PREFIX}/bin/${PROG} + install -m 644 ${PROG}.1 ${DESTDIR}${MANPREFIX}/man1/${PROG}.1 +``` + +Before installing, the program should have been built; therefore `all` must be a +prerequisite for `install`. + +The user or package maintainer can set the variable `${DESTDIR}` to specify a +different installation destination. This variable must be prepended to each +installation path; and the Makefile author should not define it (it is left to +the user or package maintainer to define it). Note that there is no bar +separating ``${DESTDIR}`` from what follows, because the `${PREFIX}` and +`${MANPREFIX}` variables should already begin with a bar. + +The Makefile author, however, is expected to define two variables pointing to +installation prefixes: `${PREFIX}`, pointing to the general installation prefix; +and `${MANPREFIX}`, pointing to the manual page installation prefix. There are +other commonly defined prefixes, such as `${bindir}`, set to `${PREFIX}/bin`. +The user or package maintainer can then invoke `make(1)` with those variables +assigned to different prefixes. On most GNU/Linux systems, for example, +`${PREFIX}` is assigned to `/usr`; and on OpenBSD, `${MANPREFIX}` is assigned to +`${PREFIX}/man` (without the `share/` part). + +The variables `${PREFIX}` and `${MANPREFIX}` are not automatically assigned, but +they can be changed by the user or package maintainer. These variables are +commonly assigned in the Makefile by the Makefile author with the `?=` operator, +which assign them only if not already defined, rather than with the common `=` +operator. Thus, the values of these variables can be inherited from the +environment, and the user need not have to assign them on each invocation. This +operator is a non-POSIX extension, however, although supported by both GNU and +BSD make implementations. + +Looking back at the installation commands, we first use `mkdir(1)` to create the +destination directories, and then use `install(1)` to install them. We could +simply call `install` with the `-D` flag, which automatically creates the +destination directories if necessary. However, this option is an extension and +is not supported by some implementations (such as FreeBSD's). Remember to +install each file with its proper permission modes with the `-m` option. + +The Makefile author can also create a uninstallation rule, which simply removes +the files from their destination directories. + +```Makefile +uninstall: + rm ${DESTDIR}${PREFIX}/bin/${PROG} + rm ${DESTDIR}${MANPREFIX}/man1/${PROG}.1 +``` + +## The Cleaning Rule + +The Makefile author can define a rule to clean the build directory and revert it +to its original state. Such rule is commonly called `clean`. It removes the +intermediate object files and the final files. As convenience, for the developer +to clean the build directory from core files that may be created by the system +during the development, the `clean` rule can also delete `.core` files. + +```Makefile +clean: + -rm -f ${OBJS} ${PROG} ${PROG:=.core} +``` + +Note that the command of this rule begins with an hyphen `-`. This causes make +to not return error (non-zero) exit status when the command fails. This is +handy, for cleaning an already cleaned build directory to not print errors. + +## The Phony Targets + +In a Makefile, some rules specify "virtual" targets which do not correspond to +any file to be created. These are the "phony" targets. The `.PHONY` special +target is used to mark its prerequisites as phony targets. In our Makefile, we +have four phony targets: `all`, `install`, `uninstall`, and `clean`. + +```Makefile +.PHONY: all clean install uninstall +``` + +## The Makefile + +In the end, our Makefile should look like this: + +```Makefile +PROG = myprog +SRCS = main.c util.c +OBJS = ${SRCS:.c=.o} + +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +all: ${PROG} + +main.o: parse.h util.h +parse.o: parse.h util.h +util.o: util.h + +.c.o: + ${CC} -I/usr/X11R6/include ${CFLAGS} ${CPPFLAGS} -c $< + +${PROG}: ${OBJS} + ${CC} -o $@ ${OBJS} -L/usr/X11R6/lib -lX11 ${LDFLAGS} + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + mkdir -p ${DESTDIR}${MANPREFIX}/man1 + install -m 755 ${PROG} ${DESTDIR}${PREFIX}/bin/${PROG} + install -m 644 ${PROG}.1 ${DESTDIR}${MANPREFIX}/man1/${PROG}.1 + +uninstall: + rm ${DESTDIR}${PREFIX}/bin/${PROG} + rm ${DESTDIR}${MANPREFIX}/man1/${PROG}.1 + +clean: + -rm -f ${OBJS} ${PROG} ${PROG:=.core} + +.PHONY: all clean install uninstall +``` + +## tl;dr + + * Define variables for the final files to be built, the source files, and the + intermediate object files created by the building process. Those are + commonly named `${PROG}`, `${SRCS}` and `${OBJS}`, respectively. + * Include in the Makefile, but do not assign them, the variables `${CFLAGS}`, + `${CPPFLAGS}`, `${LDFLAGS}` and `${DESTDIR}`. They should be assigned by the + user or package maintainer. + * Evaluate the flag variables (`${CFLAGS}`, `${CPPFLAGS}`, and `${LDFLAGS}`) + after any hardcoded flag, so the user or package maintainer can override it. + * Include the `all` and `clean` phony targets. Optionally include `install` + and `uninstall` phony targets. Always mark them as `.PHONY`. + * Do not use `$<` on anything but on the command of inference rules. + * Do not use `-D` with `install(1)`. + * Do not call `c99` or `gcc` manually. Call the command set in `${CC}` + instead. + * Assign `${PREFIX}` and `${MANPREFIX}` to the proper installation prefixes. + You can assign them with the `?=` operator for the user convenience, but + this assignment operator is not portable, although commonly supported. + +[original]: https://seninha.org/make/