Skip to content

C Makefile with two executables and a common directory

An answer to this question on Stack Overflow.

Question

I need to make a makefile which compiles two executables, cassini and saturnd

I've been having a ton of problems with this makefile I'm using. Sometimes it compiles, sometimes not.

Makefile

CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -Wpedantic -Wstrict-aliasing -I include
SRCCASSINI = $(wildcard src/cassini/*.c)
SRCSATURND = $(wildcard src/saturnd/*.c)
SRCCOMMON = $(wildcard src/common/*.c)
OBJCASSINI = $(SRCCASSINI:.c=.o)
OBJSATURND = $(SRCSATURND:.c=.o)
OBJCOMMON = $(SRCCOMMON:.c=.o)
EXEC = cassini saturnd
all: objs $(EXEC)
objs: $(OBJCOMMON)
cassini : $(OBJCASSINI)
    $(CC) -o $@ $(OBJCASSINI) $(OBJCOMMON)
saturnd : $(OBJSATURND) objs
    $(CC) -o $@ $(OBJSATURND) $(OBJCOMMON)
%.o : %.c
    $(CC) -o $@ -c $< $(CFLAGS)
clean : 
    rm -f src/*/*.o $(EXEC)
distclean : 
    rm -f src/*/*/.o $(EXEC)

Project structure

  • Include dir:

    include contains the .h files of all c files and more.

  • SRC dir:

    SRC contains 3 directories:

  1. cassini* contains all source files that should be compiled only with the cassini executable

  2. saturnd contains all source files that should be compiled only with the saturnd executable

  3. common contains all source files that should be compiled with both cassini and saturnd

Screenshot : https://prnt.sc/26brtpq

make will fill my screen with verbose output. Sometimes it compiles, sometimes not. For some reason.

Answer

The trick is not to use a makefile. I've been programming for years and still can't reliably make one of those do what I want.

Instead, you use cmake. It has an easy syntax which you can mostly copy-paste from one project to another and generates makefiles which reliably Do The Right Thing™.

To use cmake, you make a file called CMakeLists.txt in the parent directory of src. Put this in the file:

cmake_minimum_required (VERSION 3.9)
project(cassini_saturn LANGUAGES C)
SET(common_files
  src/common/g.c
  src/common/h.c
  src/common/i.c
)
add_executable(cassini
  src/cassini/a.c
  src/cassini/b.c
  src/cassini/c.c
  ${common_files}
)
target_compile_features(cassini PRIVATE cxx_std_11)
target_compile_options(cassini PRIVATE -Wall -Wextra -Wpedantic -Wstrict-aliasing)
target_include_directories(cassini PRIVATE include)
add_executable(saturnd
  src/saturnd/d.c
  src/saturnd/e.c
  src/saturnd/f.c
  ${common_files}
)
target_compile_features(saturnd PRIVATE cxx_std_11)
target_compile_options(saturnd PRIVATE -Wall -Wextra -Wpedantic -Wstrict-aliasing)
target_include_directories(saturnd PRIVATE include)

Edit the obvious places so that the files your project uses are each listed individually. It is a best practice to list all the files, rather than relying on wildcards and globs, though if you want to not follow best practices you can use, eg:

file(GLOB SRC_FILES src/common/*.c)

Now that your CMakeLists.txt file is set up, it's time to use it. To do so, you'll run the following:

mkdir build/
cd build/
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
make

You make a directory called build (it could be named anything) to contain all the byproducts of building and compiling your project. That way, you can just rm -rf build/ to get a clean slate. This is called an out-of-source build and it is a best practice.

The line

cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..

generates a makefile for your project that, when run, will build your project with optimizations and debugging info. Other options are Release (all optimizations, no debugging info) and Debug (no optimizations, debugging info).

The final line, make, runs the generated makefile and builds the executables inside the build directory.