Linux Makefile Structure and Documentation

Linux Makefile structure and documentation

The best tutorial that I have found for makefiles so far has been this one. One of your other options is to learn autotools and let them produce your makefiles for you, though a little outdated this is the tutorial that I found best to learn from.

How to document a makefile?

One nice touch is to provide a phony help target that prints a summary of targets and options. From the Linux kernel Makefile:

help:
@echo 'Cleaning targets:'
@echo ' clean - Remove most generated files but keep the config and'
@echo ' enough build support to build external modules'
@echo ' mrproper - Remove all generated files + config + various backup files'
@echo ' distclean - mrproper + remove editor backup and patch files'
@echo ''
@echo 'Configuration targets:'
@$(MAKE) -f $(srctree)/scripts/kconfig/Makefile help
@echo ''

It might be a bit of work to maintain the documentation this way, but I find it nicely separates what is intended for "users" versus what is intended for people maintaining the Makefile itself (inline comments).

What is the purpose of a makefile?

Yes it is essentially just a list of instructions. The purpose of a makefile is to be easily build an executable that might take many commands to create (which would be a pain to compile over and over again manually).

What do the makefile symbols $@ and $ mean?

$@ is the name of the target being generated, and $< the first prerequisite (usually a source file). You can find a list of all these special variables in the GNU Make manual.

For example, consider the following declaration:

all: library.cpp main.cpp

In this case:

  • $@ evaluates to all
  • $< evaluates to library.cpp
  • $^ evaluates to library.cpp main.cpp

minimum c++ make file for linux

If it is a single file, you can type

make t

And it will invoke

g++ t.cpp -o t

This doesn't even require a Makefile in the directory, although it will get confused if you have a t.cpp and a t.c and a t.java, etc etc.

Also a real Makefile:

SOURCES := t.cpp
# Objs are all the sources, with .cpp replaced by .o
OBJS := $(SOURCES:.cpp=.o)

all: t

# Compile the binary 't' by calling the compiler with cflags, lflags, and any libs (if defined) and the list of objects.
t: $(OBJS)
$(CC) $(CFLAGS) -o t $(OBJS) $(LFLAGS) $(LIBS)

# Get a .o from a .cpp by calling compiler with cflags and includes (if defined)
.cpp.o:
$(CC) $(CFLAGS) $(INCLUDES) -c $<

How does kbuild actually work?

Kbuild's Makefiles aren't the easiest to read, but here's a high-level untangling (using the 4.0-rc3 kernel):

  1. The top-level Makefile does

    include $(srctree)/scripts/Kbuild.include

    , where $(srctree) is the top-level kernel directory.

  2. Kbuild.include defines various common stuff and helpers. Among these is build:

    ###
    # Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
    # Usage:
    # $(Q)$(MAKE) $(build)=dir
    build := -f $(srctree)/scripts/Makefile.build obj

    build is used with a command like $(MAKE) $(build)=dir to perform the build for the directory dir. It makes use of scripts/Makefile.build.

  3. Returning to the top-level Makefile, there's the following:

    $(vmlinux-dirs): prepare scripts
    $(Q)$(MAKE) $(build)=$@

    vmlinux-dirs contains a list of subdirectories to build (init, usr, kernel, etc.). $(Q)$(MAKE) $(build)=<subdirectory> will be run for each subdirectory.

    The rule above compiles object files for both the kernel image and modules. Further down in the top-level Makefile, there's some additional module-specific stuff:

    ifdef CONFIG_MODULES
    ...
    modules: $(vmlinux-dirs) $(if $(KBUILD_BUILTIN),vmlinux) modules.builtin
    # Do additional module-specific stuff using
    # scripts/Makefile.modpost among other things
    # (my comment).
    ...
    ...
    endif # CONFIG_MODULES
  4. Looking into scripts/Makefile.build (the Makefile used by $(build)) now, it begins by initializing the obj-* lists and various other lists:

    # Init all relevant variables used in kbuild files so
    # 1) they have correct type
    # 2) they do not inherit any value from the environment
    obj-y :=
    obj-m :=
    lib-y :=
    lib-m :=

    A bit further down, it loads in the Kbuild file where obj-y, obj-m, etc., are set:

    include $(kbuild-file)

    Further down is the default rule, which has the $(obj-y) and $(obj-m) lists as prerequisites:

    __build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
    $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
    $(subdir-ym) $(always)
    @:

    The $(obj-y) prerequisites come from $(builtin-target), which is defined as follows:

    builtin-target := $(obj)/built-in.o
    ...
    $(builtin-target): $(obj-y) FORCE
    $(call if_changed,link_o_target)

    The actual building seems to be performed by the following rule:

    # Built-in and composite module parts
    $(obj)/%.o: $(src)/%.c $(recordmcount_source) FORCE
    $(call cmd,force_checksrc)
    $(call if_changed_rule,cc_o_c)

    if_changed_rule is from Kbuild.include. The rule ends up running the following commands in Makefile.build:

    define rule_cc_o_c
    $(call echo-cmd,checksrc) $(cmd_checksrc) \
    $(call echo-cmd,cc_o_c) $(cmd_cc_o_c); \
    ...
    endef

    $(cmd_cc_o_c) seems to be the actual compilation command. The usual definition (there are two possibilities in Makefile.build, AFAICS) seems to be the following:

    cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $<

    Unless set explicitly using e.g. make CC=clang, CC defaults to gcc, as can be seen here in the top-level Makefile:

    ifneq ($(CC),)
    ifeq ($(shell $(CC) -v 2>&1 | grep -c "clang version"), 1)
    COMPILER := clang
    else
    COMPILER := gcc
    endif
    export COMPILER
    endif

The way I untangled this was by doing a CTRL-C during a kernel build and seeing where make reported the error. Another handy make debugging technique is to use $(warning $(variable)) to print the value of variable.

Building C-program out of source tree with GNU make

Here's the Makefile I've added to the documentation (currently in review so I'll post it here) :

# Set project directory one level above the Makefile directory. $(CURDIR) is a GNU make variable containing the path to the current working directory
PROJDIR := $(realpath $(CURDIR)/..)
SOURCEDIR := $(PROJDIR)/Sources
BUILDDIR := $(PROJDIR)/Build

# Name of the final executable
TARGET = myApp.exe

# Decide whether the commands will be shown or not
VERBOSE = TRUE

# Create the list of directories
DIRS = Folder0 Folder1 Folder2
SOURCEDIRS = $(foreach dir, $(DIRS), $(addprefix $(SOURCEDIR)/, $(dir)))
TARGETDIRS = $(foreach dir, $(DIRS), $(addprefix $(BUILDDIR)/, $(dir)))

# Generate the GCC includes parameters by adding -I before each source folder
INCLUDES = $(foreach dir, $(SOURCEDIRS), $(addprefix -I, $(dir)))

# Add this list to VPATH, the place make will look for the source files
VPATH = $(SOURCEDIRS)

# Create a list of *.c sources in DIRS
SOURCES = $(foreach dir,$(SOURCEDIRS),$(wildcard $(dir)/*.c))

# Define objects for all sources
OBJS := $(subst $(SOURCEDIR),$(BUILDDIR),$(SOURCES:.c=.o))

# Define dependencies files for all objects
DEPS = $(OBJS:.o=.d)

# Name the compiler
CC = gcc

# OS specific part
ifeq ($(OS),Windows_NT)
RM = del /F /Q
RMDIR = -RMDIR /S /Q
MKDIR = -mkdir
ERRIGNORE = 2>NUL || true
SEP=\\
else
RM = rm -rf
RMDIR = rm -rf
MKDIR = mkdir -p
ERRIGNORE = 2>/dev/null
SEP=/
endif

# Remove space after separator
PSEP = $(strip $(SEP))

# Hide or not the calls depending of VERBOSE
ifeq ($(VERBOSE),TRUE)
HIDE =
else
HIDE = @
endif

# Define the function that will generate each rule
define generateRules
$(1)/%.o: %.c
@echo Building $$@
$(HIDE)$(CC) -c $$(INCLUDES) -o $$(subst /,$$(PSEP),$$@) $$(subst /,$$(PSEP),$$<) -MMD
endef

# Indicate to make which targets are not files
.PHONY: all clean directories

all: directories $(TARGET)

$(TARGET): $(OBJS)
$(HIDE)echo Linking $@
$(HIDE)$(CC) $(OBJS) -o $(TARGET)

# Include dependencies
-include $(DEPS)

# Generate rules
$(foreach targetdir, $(TARGETDIRS), $(eval $(call generateRules, $(targetdir))))

directories:
$(HIDE)$(MKDIR) $(subst /,$(PSEP),$(TARGETDIRS)) $(ERRIGNORE)

# Remove all objects, dependencies and executable files generated during the build
clean:
$(HIDE)$(RMDIR) $(subst /,$(PSEP),$(TARGETDIRS)) $(ERRIGNORE)
$(HIDE)$(RM) $(TARGET) $(ERRIGNORE)
@echo Cleaning done !

Main features

  • Automatic detection of C sources in specified folders
  • Multiple source folders
  • Multiple corresponding target folders for object and dependency files
  • Automatic rule generation for each target folder
  • Creation of target folders when they don't exist
  • Dependency management with gcc : Build only what is necessary
  • Works on Unix and DOS systems
  • Written for GNU Make

How to use this Makefile

To adapt this Makefile to your project you have to :

  1. Change the TARGET variable to match your target name
  2. Change the name of the Sources and Build folders in SOURCEDIR and BUILDDIR
  3. Change the verbosity level of the Makefile in the Makefile itself or in make call (make all VERBOSE=FALSE)
  4. Change the name of the folders in DIRS to match your sources and build folders
  5. If required, change the compiler and the flags

In this Makefile Folder0, Folder1 and Folder2 are the equivalent to your FolderA, FolderB and FolderC.

Note that I have not had the opportunity to test it on a Unix system at the moment but it works correctly on Windows.


Explanation of a few tricky parts :

Ignoring Windows mkdir errors

ERRIGNORE = 2>NUL || true

This has two effects :
The first one, 2>NUL is to redirect the error output to NUL, so as it does not comes in the console.

The second one, || true prevents the command from rising the error level. This is Windows stuff unrelated with the Makefile, it's here because Windows' mkdir command rises the error level if we try to create an already-existing folder, whereas we don't really care, if it does exist that's fine. The common solution is to use the if not exist structure, but that's not UNIX-compatible so even if it's tricky, I consider my solution more clear.


Creation of OBJS containing all object files with their correct path

OBJS := $(subst $(SOURCEDIR),$(BUILDDIR),$(SOURCES:.c=.o))

Here we want OBJS to contain all the object files with their paths, and we already have SOURCES which contains all the source files with their paths.
$(SOURCES:.c=.o) changes *.c in *.o for all sources, but the path is still the one of the sources.
$(subst $(SOURCEDIR),$(BUILDDIR), ...) will simply subtract the whole source path with the build path, so we finally have a variable that contains the .o files with their paths.


Dealing with Windows and Unix-style path separators

SEP=\\
SEP = /
PSEP = $(strip $(SEP))

This only exist to allow the Makefile to work on Unix and Windows, since Windows uses backslashes in path whereas everyone else uses slashes.

SEP=\\ Here the double backslash is used to escape the backslash character, which make usually treats as an "ignore newline character" to allow writing on multiple lines.

PSEP = $(strip $(SEP)) This will remove the space char of the SEP variable, which has been added automatically.


Automatic generation of rules for each target folder

define generateRules
$(1)/%.o: %.c
@echo Building $$@
$(HIDE)$(CC) -c $$(INCLUDES) -o $$(subst /,$$(PSEP),$$@) $$(subst /,$$(PSEP),$$<) -MMD
endef

That's maybe the trick that is the most related with your usecase. It's a rule template that can be generated with $(eval $(call generateRules, param)) where param is what you can find in the template as $(1).
This will basically fill the Makefile with rules like this for each target folder :

path/to/target/%.o: %.c
@echo Building $@
$(HIDE)$(CC) -c $(INCLUDES) -o $(subst /,$(PSEP),$@) $(subst /,$(PSEP),$<) -MMD

How do you get the list of targets in a makefile?

Note: This answer has been updated to still work as of GNU make v4.3 - let us know if you come across something that breaks.

This is an attempt to improve on Brent Bradburn's great approach as follows:

  • uses a more robust command to extract the target names, which hopefully prevents any false positives (and also does away with the unnecessary sh -c)
  • does not invariably target the makefile in the current directory; respects makefiles explicitly specified with -f <file>
  • excludes hidden targets - by convention, these are targets whose name starts neither with a letter nor a digit
  • makes do with a single phony target
  • prefixes the command with @ to prevent it from being echoed before execution

Curiously, GNU make has no feature for listing just the names of targets defined in a makefile. While the -p option produces output that includes all targets, it buries them in a lot of other information and also executes the default target (which could be suppressed with -f/dev/null).

Place the following rule in a makefile for GNU make to implement a target named list that simply lists all target names in alphabetical order - i.e.: invoke as make list:

.PHONY: list
list:
@LC_ALL=C $(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/(^|\n)# Files(\n|$$)/,/(^|\n)# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$'

Important: On pasting this, make sure that the last line is indented by exactly 1 actual tab char. (spaces do not work).

Note that sorting the resulting list of targets is the best option, since not sorting doesn't produce a helpful ordering in that the order in which the targets appear in the makefile is not preserved.

Also, the sub-targets of a rule comprising multiple targets are invariably output separately and will therefore, due to sorting, usually not appear next to one another; e.g., a rule starting with a z: will not have targets a and z listed next to each other in the output, if there are additional targets.

Explanation of the rule:

  • .PHONY: list

    • declares target list a phony target, i.e., one not referring to a file, which should therefore have its recipe invoked unconditionally
  • LC_ALL=C makes sure that make's output in in English, as parsing of the output relies on that.Tip of the hat to Bastian Bittorf

  • $(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null

    • Invokes make again in order to print and parse the database derived from the makefile:
      • -p prints the database
      • -Rr suppresses inclusion of built-in rules and variables
      • -q only tests the up-to-date-status of a target (without remaking anything), but that by itself doesn't prevent execution of recipe commands in all cases; hence:
      • -f $(lastword $(MAKEFILE_LIST)) ensures that the same makefile is targeted as in the original invocation, regardless of whether it was targeted implicitly or explicitly with -f ....

        Caveat: This will break if your makefile contains include directives; to address this, define variable THIS_FILE := $(lastword $(MAKEFILE_LIST)) before any include directives and use -f $(THIS_FILE) instead.
      • : is a deliberately invalid target that is meant to ensure that no commands are executed; 2>/dev/null suppresses the resulting error message. Note: This relies on -p printing the database nonetheless, which is the case as of GNU make 3.82. Sadly, GNU make offers no direct option to just print the database, without also executing the default (or given) task; if you don't need to target a specific Makefile, you may use make -p -f/dev/null, as recommended in the man page.
  • -v RS=

    • This is an awk idiom that breaks the input into blocks of contiguous non-empty lines.
  • /(^|\n)# Files(\n|$$)/,/(^|\n)# Finished Make data base/

    • Matches the range of lines in the output that contains all targets, across paragraphs - by limiting parsing to this range, there is no need to deal with false positives from other output sections.
    • Note: Between make versions 3.x and 4.3, paragraph structuring in make's output changed, so (^|\n) / (\n|$$) ensures that the lines that identify the start and the end of the cross-paragraph range of lines of interest are detected irrespective of whether they occur at the start or inside / at the end of a paragraph.
  • if ($$1 !~ "^[#.]")

    • Selectively ignores blocks:
      • # ... ignores non-targets, whose blocks start with # Not a target:
      • . ... ignores special targets
    • All other blocks should each start with a line containing only the name of an explicitly defined target followed by :
  • egrep -v -e '^[^[:alnum:]]' -e '^$@$$' removes unwanted targets from the output:

    • '^[^[:alnum:]]' ... excludes hidden targets, which - by convention - are targets that start neither with a letter nor a digit.
    • '^$@$$' ... excludes the list target itself

Running make list then prints all targets, each on its own line; you can pipe to xargs to create a space-separated list instead.



Related Topics



Leave a reply



Submit