Building C-program “out of source tree” with GNU make

后端 未结 4 958
广开言路
广开言路 2020-11-28 08:36

I would like to build a C-project for my microcontroller with the GNU make tool. I would like to do it in a clean way, such that my source code is not cluttered with object

相关标签:
4条回答
  • 2020-11-28 08:51

    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
    
    0 讨论(0)
  • 2020-11-28 08:53

    This fairly minimal makefile should do the trick:

    VPATH = ../source
    OBJS = FolderA/fileA1.o FolderA/fileA2.o FolderB/fileB1.o
    CPPFLAGS = -MMD -MP
    
    all: init myProgram
    
    myProgram: $(OBJS)
            $(CC) $(LDFLAGS) -o $@ $(OBJS) $(LDLIBS)
    
    .PHONY: all init
    
    init:
            mkdir -p FolderA
            mkdir -p FolderB
    
    -include $(OBJS:%.o=%.d)
    

    The main tricky part is ensuring that FolderA and FolderB exist in the build directory bfore trying to run the compiler that will write into them. The above code will work sequential for builds, but might fail with -j2 the first time it is run, as the compiler in one thread might try to open an output file before the other thread creates the directory. Its also somewhat unclean. Usually with GNU tools you have a configure script that will create those directories (and the makefile) for you before you even try to run make. autoconf and automake can build that for you.

    An alternate way that should work for parallel builds would be to redefine the standard rule for compiling C files:

    VPATH = ../source
    OBJS = FolderA/fileA1.o FolderA/fileA2.o FolderB/fileB1.o
    CPPFLAGS = -MMD -MP
    
    myProgram: $(OBJS)
            $(CC) $(LDFLAGS) -o $@ $(OBJS) $(LDLIBS)
    
    %.o: %.c
            mkdir -p $(dir $@)
            $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
    
    -include $(OBJS:%.o=%.d)
    

    Which has the disadvantage that you'll also need to redefine the builtin rules for any other kind of sourcefile you want to compile

    0 讨论(0)
  • 2020-11-28 08:53

    I would avoid manipulating Makefile directly, and use CMake instead. Just describe your source files in CMakeLists.txt, as below:

    Create file MyProject/source/CMakeLists.txt containing;

    project(myProject)
    add_executable(myExec FolderA/fileA1.c FolderA/fileA2.c FolderB/fileB1.c)
    

    Under MyProject/build, run

    cmake ../source/
    

    You'll get a Makefile now. To build, under the same build/ directory,

    make
    

    You may also want to switch to a lightning fast build tool, ninja, simply by adding a switch as following.

    cmake -GNinja ..
    ninja
    
    0 讨论(0)
  • 2020-11-28 09:06

    Here's a basic one I use all the time, it's pretty much a skeleton as it is but works perfectly fine for simple projects. For more complex projects it certainly needs to be adapted, but I always use this one as a starting point.

    APP=app
    
    SRC_DIR=src
    INC_DIR=inc
    OBJ_DIR=obj
    BIN_DIR=bin
    
    CC=gcc
    LD=gcc
    CFLAGS=-O2 -c -Wall -pedantic -ansi
    LFLGAS=
    DFLAGS=-g3 -O0 -DDEBUG
    INCFLAGS=-I$(INC_DIR)
    
    SOURCES=$(wildcard $(SRC_DIR)/*.c)
    HEADERS=$(wildcard $(INC_DIR)/*.h)
    OBJECTS=$(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
    DEPENDS=$(OBJ_DIR)/.depends
    
    
    .PHONY: all
    all: $(BIN_DIR)/$(APP)
    
    .PHONY: debug
    debug: CFLAGS+=$(DFLAGS)
    debug: all
    
    
    $(BIN_DIR)/$(APP): $(OBJECTS) | $(BIN_DIR)
        $(LD) $(LFLGAS) -o $@ $^
    
    $(OBJ_DIR)/%.o: | $(OBJ_DIR)
        $(CC) $(CFLAGS) $(INCFLAGS) -o $@ $<
    
    $(DEPENDS): $(SOURCES) | $(OBJ_DIR)
        $(CC) $(INCFLAGS) -MM $(SOURCES) | sed -e 's!^!$(OBJ_DIR)/!' >$@
    
    ifneq ($(MAKECMDGOALS),clean)
    -include $(DEPENDS)
    endif
    
    
    $(BIN_DIR):
        mkdir -p $@
    $(OBJ_DIR):
        mkdir -p $@
    
    .PHONY: clean
    clean:
        rm -rf $(BIN_DIR) $(OBJ_DIR)
    
    0 讨论(0)
提交回复
热议问题