DA MASER A MONFUMO | MOTO GUZZI V85TT | PURE SOUND POV 4K

🏍️ Il mio nuovo canale YouTube: giri in moto in POV, solo audio, tra le Dolomiti in 4K. Niente musica, niente parole — solo il motore e le Alpi. Vieni a fare un giro!

Iscriviti

Nel mondo dello sviluppo software, specialmente nei progetti C, un Makefile è il vostro progetto su come il progetto viene compilato e organizzato. È un modo semplice per gestire il processo di build, dettando come il codice sorgente si trasforma in un programma eseguibile. Le alternative includono sistemi di build come CMake o Meson, ma i Makefile rimangono una scelta popolare grazie alla loro natura diretta. In un Makefile, si può stampare a schermo usando il comando echo, utilizzare wildcard per specificare gruppi di file e impiegare target 'phony' per eseguire comandi indipendentemente dalle dipendenze dei file.

Detto questo, nel mio progetto ho mirato a creare un Makefile che fosse pulito, manutenibile e facile da comprendere. Ho adottato un approccio sistematico per raggiungerlo, e il risultato si trova nel repository gnaro su GitHub.

Variabili

makefile
debug ?= 0
NAME := gnaro
SRC_DIR := src
BUILD_DIR := build
INCLUDE_DIR := include
LIB_DIR := lib
TESTS_DIR := tests
BIN_DIR := bin

Questa sezione definisce le impostazioni generali del progetto:

  • debug: Un flag condizionale per la build in modalità debug. Viene usato per determinare se abilitare i flag di debugging nel processo di build.
  • NAME: Il nome del progetto.
  • Da SRC_DIR a BIN_DIR: Le directory per i file sorgente, l'output della build, gli include, le librerie, i test e i binari.

Percorsi dei File Oggetto

makefile
OBJS := $(patsubst %.c,%.o, $(wildcard $(SRC_DIR)/*.c) $(wildcard $(LIB_DIR)/**/*.c))

Questa riga è responsabile della generazione dei percorsi per tutti i file oggetto:

  • Usa wildcard per trovare tutti i file .c in SRC_DIR e LIB_DIR.
  • Trasforma ogni nome di file .c nel corrispondente nome di file .o usando patsubst.

I file oggetto sono file intermedi generati durante il processo di build. Vengono usati per memorizzare il codice compilato di ogni file sorgente, e vengono collegati insieme per creare l'eseguibile finale. Il Makefile presuppone che la struttura delle directory all'interno della directory build rispecchi quella della directory root (solo per i file rilevanti al processo di build).

Impostazioni del Compilatore

makefile
CC := clang-18
LINTER := clang-tidy-18
FORMATTER := clang-format-18

Qui impostiamo il compilatore su clang-18 e definiamo gli strumenti per il linting (clang-tidy-18) e la formattazione (clang-format-18).

Impostazioni dei Flag del Compilatore e del Linker

makefile
CFLAGS := -std=gnu17 -D _GNU_SOURCE -D __STDC_WANT_LIB_EXT1__ -Wall -Wextra -pedantic
LDFLAGS := -lm

Questa sezione definisce:

  • CFLAGS: Flag di compilazione, che specificano:
    • Lo standard C da usare (gnu17).
    • Definizioni per abilitare le estensioni GNU e C11.
    • Vari flag di warning.
  • LDFLAGS: Flag del linker, come il collegamento alla libreria matematica (libm).
makefile
ifeq ($(debug), 1)
	CFLAGS := $(CFLAGS) -g -O0
else
	CFLAGS := $(CFLAGS) -Oz
endif

Questo blocco condizionale aggiunge i flag di debugging (-g -O0) a CFLAGS se debug è impostato a 1. Altrimenti, imposta il livello di ottimizzazione a -Oz.

Target

makefile
$(NAME): format lint dir $(OBJS)
	$(CC) $(CFLAGS) $(LDFLAGS) -o $(BIN_DIR)/$@ $(patsubst %, build/%, $(OBJS))

Questo target è quello che genera l'eseguibile finale per gnaro. Garantisce anche che il codice sorgente venga formattato e lintato prima della build. La variabile $(OBJS) viene usata per specificare i file oggetto da collegare insieme.

I file oggetto vengono compilati usando il seguente target:

makefile
$(OBJS): dir
	@mkdir -p $(BUILD_DIR)/$(@D)
	@$(CC) $(CFLAGS) -o $(BUILD_DIR)/$@ -c $*.c

Ogni file oggetto dipende dal corrispondente file sorgente. Questo target compila ogni file sorgente nel suo file oggetto.

Esecuzione dei Test

makefile
test: dir
	@$(CC) $(CFLAGS) -lcunit -o $(BIN_DIR)/$(NAME)_test $(TESTS_DIR)/*.c
	@$(BIN_DIR)/$(NAME)_test

Questo target compila ed esegue i test usando il framework di test CUnit.

Linting, Formattazione e Controllo della Memoria

I target lint, format e check gestiscono i controlli della qualità del codice usando gli strumenti specificati.

makefile
# Run CUnit tests
test: dir
	@$(CC) $(CFLAGS) -lcunit -o $(BIN_DIR)/$(NAME)_test $(TESTS_DIR)/*.c
	@$(BIN_DIR)/$(NAME)_test
 
# Run linter on source directories
lint:
	@$(LINTER) --config-file=.clang-tidy $(SRC_DIR)/* $(INCLUDE_DIR)/* $(TESTS_DIR)/* -- $(CFLAGS)
 
# Run formatter on source directories
format:
	@$(FORMATTER) -style=file -i $(SRC_DIR)/* $(INCLUDE_DIR)/* $(TESTS_DIR)/*
 
# Run valgrind memory checker on executable
check: $(NAME)
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< --help
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< --version
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< -v

Installazione delle Dipendenze

makefile
setup:
	# ... OS and tool installation commands ...

Ho incluso un target per configurare il progetto e installare i pacchetti OS e gli strumenti di sviluppo necessari su un nuovo sistema. Inizialmente usavo script per automatizzare questo processo, ma ho trovato più pulito includerlo nel Makefile.

Configurazione delle Directory e Pulizia

Il target dir garantisce che le directory di build e bin esistano, mentre il target clean le rimuove.

makefile
# Setup build and bin directories
dir:
	@mkdir -p $(BUILD_DIR) $(BIN_DIR)
 
# Clean build and bin directories
clean:
	@rm -rf $(BUILD_DIR) $(BIN_DIR)

Target Phony

makefile
.PHONY: lint format check setup dir clean bear

.PHONY dice a make che i target elencati non corrispondono a file reali. Questo garantisce che vengano sempre eseguiti, anche se esiste un file con lo stesso nome.

Bear

Bear è uno strumento che genera un database di compilazione per gli strumenti clang. Viene usato per generare un file compile_commands.json, che viene usato da strumenti come clang-tidy e clang-format per determinare come gestire i file sorgente.

makefile
bear --exclude $(LIB_DIR) make $(NAME)

Makefile Completo

Includo di seguito il Makefile completo come riferimento:

makefile
# Project Settings
debug ?= 0
NAME := gnaro
SRC_DIR := src
BUILD_DIR := build
INCLUDE_DIR := include
LIB_DIR := lib
TESTS_DIR := tests
BIN_DIR := bin
 
# Generate paths for all object files
OBJS := $(patsubst %.c,%.o, $(wildcard $(SRC_DIR)/*.c) $(wildcard $(LIB_DIR)/**/*.c))
 
# Compiler settings
CC := clang-18
LINTER := clang-tidy-18
FORMATTER := clang-format-18
 
# Compiler and Linker flags Settings:
# 	-std=gnu17: Use the GNU17 standard
# 	-D _GNU_SOURCE: Use GNU extensions
# 	-D __STDC_WANT_LIB_EXT1__: Use C11 extensions
# 	-Wall: Enable all warnings
# 	-Wextra: Enable extra warnings
# 	-pedantic: Enable pedantic warnings
# 	-lm: Link to libm
CFLAGS := -std=gnu17 -D _GNU_SOURCE -D __STDC_WANT_LIB_EXT1__ -Wall -Wextra -pedantic
LDFLAGS := -lm
 
ifeq ($(debug), 1)
	CFLAGS := $(CFLAGS) -g -O0
else
	CFLAGS := $(CFLAGS) -Oz
endif
 
# Targets
 
# Build executable
$(NAME): format lint dir $(OBJS)
	$(CC) $(CFLAGS) $(LDFLAGS) -o $(BIN_DIR)/$@ $(patsubst %, build/%, $(OBJS))
 
# Build object files and third-party libraries
$(OBJS): dir
	@mkdir -p $(BUILD_DIR)/$(@D)
	@$(CC) $(CFLAGS) -o $(BUILD_DIR)/$@ -c $*.c
 
# Run CUnit tests
test: dir
	@$(CC) $(CFLAGS) -lcunit -o $(BIN_DIR)/$(NAME)_test $(TESTS_DIR)/*.c
	@$(BIN_DIR)/$(NAME)_test
 
# Run linter on source directories
lint:
	@$(LINTER) --config-file=.clang-tidy $(SRC_DIR)/* $(INCLUDE_DIR)/* $(TESTS_DIR)/* -- $(CFLAGS)
 
# Run formatter on source directories
format:
	@$(FORMATTER) -style=file -i $(SRC_DIR)/* $(INCLUDE_DIR)/* $(TESTS_DIR)/*
 
# Run valgrind memory checker on executable
check: $(NAME)
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< --help
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< --version
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< -v
 
# Setup dependencies for build and development
setup:
	# Update apt and upgrade packages
	@sudo apt update
	@sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y
 
	# Install OS dependencies
	@sudo apt install -y bash libarchive-tools lsb-release wget software-properties-common gnupg
 
	# Install LLVM tools required for building the project
	@wget https://apt.llvm.org/llvm.sh
	@chmod +x llvm.sh
	@sudo ./llvm.sh 18
	@rm llvm.sh
 
	# Install Clang development tools
	@sudo apt install -y clang-tools-18 clang-format-18 clang-tidy-18 valgrind bear
 
	# Install CUnit testing framework
	@sudo apt install -y libcunit1 libcunit1-doc libcunit1-dev
 
	# Cleanup
	@sudo apt autoremove -y
 
# Setup build and bin directories
dir:
	@mkdir -p $(BUILD_DIR) $(BIN_DIR)
 
# Clean build and bin directories
clean:
	@rm -rf $(BUILD_DIR) $(BIN_DIR)
 
# Run bear to generate compile_commands.json
bear:
	bear --exclude $(LIB_DIR) make $(NAME)
 
.PHONY: lint format check setup dir clean bear

Limitazioni

Sebbene il Makefile del progetto gnaro offra un processo di build strutturato e completo, presenta alcune limitazioni. Per esempio, è pensato specificamente per ambienti con il package manager apt, rendendolo meno portabile su sistemi non basati su Debian. Il Makefile presuppone anche la disponibilità di versioni specifiche di strumenti come clang-18, il che potrebbe porre delle sfide se il progetto viene spostato in ambienti con versioni diverse degli strumenti o se questi vengono deprecati in futuro. Inoltre, la forte dipendenza da regole implicite e directory predefinite potrebbe rendere il Makefile meno flessibile ai cambiamenti nella struttura del progetto o ai requisiti di build. Tuttavia, queste limitazioni sono compensate dai vantaggi di avere un Makefile pulito e manutenibile.

Conclusione

Creare un Makefile pulito e manutenibile non deve necessariamente essere un'impresa complessa. Sfruttando le funzionalità dei Makefile come variabili, wildcard, variabili automatiche e target phony, e commentando liberalmente, sono riuscito a creare un Makefile che non è solo funzionale, ma anche facile da comprendere e modificare. Questo approccio diretto ha semplificato il processo di build del mio progetto C, dimostrando l'utilità e la semplicità dei Makefile nella gestione dei processi di build.