zero

Building C Projects: a guide for normies

I have been programming in C (not C++) for the past six years. In fact, C was my first programming language. I learned C from the book The C Programming Language, written by the brilliant Dennis Ritche himself. I worked for some time on Redis and did lots of small projects for fun. So I believe I am eligible to give my own unsolicited opinion and make it part of the mountain of articles that already exist on this topic.

First of all, stop using programs or so-called "build systems" like Ninja or CMake to build projects. I cannot comprehend this. GNU Make is still bearable but why use something as obscure as CMake?! Not only is it as fat as your mom, it is also a burden. CMake has its own scripting language which has to be learned by you, the fellow programmer. CMake may appear sexy as big projects use it, but even for a project as large as Redis, such build systems prove useless.

Now if not CMake, then what? The answer is Bash, PowerShell and Python. Bash and Powershell have their own scripting language. You can write a bash script (<script>.sh) easily and the syntax will be very familiar to you if you use a Linux machine on a daily basis — which I assume you do because you are writing C code. Programmers working on Windows have three options: write a PowerShell (<script>.ps1) or command prompt batch (<script>.bat) script, start using Visual Studio like a sociopath, or start using Linux. If you are a college student though, I will highly recommend you to use Linux even on your personal computer. It may not run Valorant but you will realize how important that word appears in a résumé. When working with server-side applications, you will face Linux all the time.

My own build.sh from one of my projects is located in the root directory. It looks somewhat like this:

#!/bin/bash

# Variables
CC="c99"
SRC_DIR="src"         # Source directory containing C files
BUILD_DIR="build"     # Directory to store object files
EXECUTABLE_DIR="bin"  # Name of the final executable

# Create the build directory if it doesn't exist
mkdir -p $BUILD_DIR
mkdir -p $EXECUTABLE_DIR

# Clean previous builds
rm -f $BUILD_DIR/*.o
rm -f $EXECUTABLE_DIR/*

# Compile each C file in the src directory
for file in $SRC_DIR/*.c; do
    # Get the filename without the path and extension
    filename=$(basename "$file" .c)

    # Compile the file into an object file
    gcc -c "$file" -o "$BUILD_DIR/$filename.o" -Wall -Wextra -Wpedantic

    # Check for compilation errors
    if [ $? -ne 0 ]; then
        echo "Error compiling $file"
        exit 1
    fi
done

# Link all object files into a single executable
$CC $BUILD_DIR/*.o -o "$EXECUTABLE_DIR/main.out" -lm -Wall -Wextra -Wpedantic

# Check for linking errors
if [ $? -ne 0 ]; then
    echo "Error linking object files"
    exit 1
fi

echo "Build successful! Executable created: $EXECUTABLE_DIR/main.out"

This may look intimidating at first but once you get comfortable, things will become very easy. Mind that it matters how you structure your project. For example, my project structure follows the follow tree:

.
├── bin
│   └── main.out
├── build
│   ├── main.o
│   ├── matrix.o
│   ├── plane.o
│   ├── vector.o
│   └── <object files of other source files>
├── build.sh
└── src
    ├── main.c
    ├── matrix.h
    ├── matrix.c
    ├── plane.c
    ├── plane.h
    ├── vector.c
    ├── vector.h
    └── <other source code files>

As you can see, all my source code is located in src/. Since C compilers generate object files during compilation, I have set their output directory to build/ and binaries into bin/. You can have your own configurations. Maybe even drop testing capabilities by adding a test/ folder with test files and add an option to execute ./build.sh test to run the files in test/.

If you are not comfortable with Bash, you can use Python too. In fact, Python provides more convenience to the programmer to make the build file extensible with lots of other cool features. For example, different modes of compilations such as 'test' and 'prod'. In this manner, the end user does not have to rely on a third-party dependency like CMake to use your code. So it's a headache less even for the user.

#blog #code