Recientemente decidí iniciar un nuevo proyecto (ya daré más detalles sobre él más adelante) y para poner las cosas un poco más interesantes me decidí a desempolvar el libro C++ Programming Language de Bjarne Stroustrup y el libro de Thinking in C++ de Bruce Eckel y empecé a codificar en C++. Fue una vuelta al pasado pues hacía lustros que no desarrollaba con C++; de hecho, creo recordar que salvo un año que estuve trabajando con HP-UX, iLOG y las STL de RogeWave, casi toda mi experiencia en este lenguaje ha sido dentro de Visual Studio (lo cual simplifica mucho la vida cuando tienes que bregar con las cuestiones que refiero en el título del post)
Bueno, entrando en materia, sabréis que una de las cuestiones que mas trabajo requiere es organizar cómo va a ser tu proceso de compilación (o «build» como me gusta más llamarlo), qué estructura de directorios albergará tu proyecto y con qué herramientas vas a trabajar tanto para codificar como para compilar (y ya como bonus, para probar tu código)
De épocas pasadas recordé que solía utilizar «Makefiles» y ni corto ni perezoso me decidí a tirar por ahí.
Ventajas y desventajas de make y sus Makefiles
make es una herramienta que viene desde tiempos inmemoriales solucionando la cuestión de cómo hacer la «build» de casi cualquier tipo de proyecto. De manera breve, make trabaja definiendo «targets«, «dependencies» y «recipes«. Los «targets» son los objetivos o resultados que queremos obtener en el proceso de build (sí, también los resultados intermedios pueden ser «targets«), Las «dependencies» son las dependencias (a cualquier nivel: binarias, intermedios de compilación, librerías, e incluso de código fuente) necesarias para obtener cada uno de los «targets«, y por último, las «recipes» son las recetas para «cocinar» el «target» a partir (normalmente) de las «dependencies» (perdón por el símil culinario, pero seguro que así la terminología «oficial» de make tiene más sentido para todos)
make, además, viene casi de serie en cualquier distribución de Unix, Linux, e incluso en Windows, y como tiene tanta historia, casi cualquier desarrollador que se precie, sabe algo acerca de «makefiles«. Por generar controversia, un desarrollador que no se haya pegado con un «Makefile» es como uno que no ha usado «vi» o «emacs«.
Las desventajas, entre muchas y variadas, están en que no es precisamente una herramienta sencilla de exprimir toda su potencia, y tampoco es que su comportamiento sea homogéneo en cualquier S.O. De hecho, os animo a que intentéis que un proceso de «build» basado en «Makefiles» hecho en un S.O. se ejecute sin problemas en otro S.O. totalmente distinto (yo lo he probado de Linux a Windows y ha sido un sufrimiento, aunque finalmente lo puedes conseguir)
Opciones a make y sus «Makefiles»
Mucha gente os dirá que las tecnologías de compilación han mejorado mucho desde hace tiempo, y parte de razón tienen. En concreto os encontrareis que mucha gente utiliza soluciones como CMake, Ant, Maven, Gradle, etc. Todas con sus ventajas y sus inconvenientes. Principalmente el problema que le veo a todas ellas es que no son «make«; es decir, que cada una supone una curva de aprendizaje específica, están muy orientadas a determinadas plataformas y fundamentalmente todas son variantes más o menos enrevesadas de «make». Bueno, salvo CMake que funciona de una manera muy «alternativa» y que puede que por eso se pusiera tan de moda hace unos años en la comunidad C/C++
Volviendo a make
En fin, que después de varios intentos con IDEs y herramientas de compilación (incluido CMake y Maven entre otros), me dije que cualquier tiempo pasado fue mejor y que tenía que conseguir algo productivo con make. ¿Dónde estaban mis dificultades principalmente? Pues en que no quería tener que declarar mis dependencias de código fuente en un Makefile.
Cuando desarrollas en casi cualquier lenguaje, y en especial en C/C++, tus ficheros de código incluyen referencias a otros ficheros de código. Por ejemplo, mi fichero de aplicación Main.cpp
#include "Vector2D.h" #include "SimulEngine.h" #include <iostream> using namespace std; int main (void) { std::cout << "Hello World!" << std::endl; } |
Incluye varias dependencias a otros ficheros (Vector2D.h y SimulEngine.h) que a su vez incluirán las suyas, etc. Esto hace que normalmente en los Makefiles tengas que declarar tu mapa de dependencias de código de manera explícita. Vale, realmente no es así, porque make tiene mucha inteligencia previa en forma de «reglas implícitas«, pero esa inteligencia es bastante básica (p.e. sabe que para generar un fichero objeto, tiene que compilar el mismo fichero pero con extensión «.cpp» pero no mucho más) De ahí que me decidiera a ver cómo podemos hacer que nuestro proceso de compilación no tenga que conocer nada acerca de las dependencias de código del proyecto. Y aquí es donde nuestros maravillosos compiladores de C/C++ (en concreto en mi caso GNU C++) vienen en nuestra ayuda.
En concreto, si miráis dentro de la documentación oficial de GNU Make, veréis que hay un apartado específico sobre Generación Automática de Dependencias Leyendo con cuidado y acudiendo además a la documentación de referencia de GNU CC (en mi caso la versión 7.03) sobre control del preprocesador veremos que podemos generar automáticamente un mapa de dependencias de código que a su vez podemos incluir como parte de nuestro proceso de compilación. El problema es que make es bastante denso y acabar con un buen Makefile que tenga todo esto en cuenta es bastante complicado. Por ello, os dejo mi Makefile para que podáis estudiarlo y adaptarlo a vuestras necesidades en el siguiente apartado.
Make (y su Makefile) con generación de dependencias automática
Dejadme que primero os describa mi estructura de directorios para el proyecto. Es bastante simple, y está basada múltiples referencias aunque si queréis podéis leer al respecto en Stackoverflow. Aquí la tenéis un poco resumida:
myproject/ include/ src/ build/ bin/ doc/ Makefile README.md .gitignore |
Teniendo esto en cuenta, os paso a continuación cómo queda mi fichero de Makefile
EXEFILE := myproject SRCDIR := src SRCS := $(wildcard ${SRCDIR}/*.cpp ) DISTFILES := ${EXEFILE} DISTOUTPUT := ${EXEFILE}.tar.gz BUILDDIR := build BINDIR := bin DEPDIR := .d INC_DIR := include INCLUDE_DIRS := $(INC_DIR) \ /usr/include \ /usr/local/include INC_FLAGS := $(addprefix -I ,$(INCLUDE_DIRS)) OBJS := $(patsubst ${SRCDIR}/%,${BUILDDIR}/%,$(patsubst %,%.o,$(basename ${SRCS}))) DEPS := $(patsubst %.o,%.d,${OBJS}) CC := gcc CXX := g++ LD := g++ TAR := tar # C flags CFLAGS := -std=c11 CXXFLAGS := -std=c++11 CPPFLAGS := -g LDFLAGS := DEPFLAGS = -MT $@ -MMD -MP -MF ${DEPDIR}/$*.Td # compile C source files COMPILE.c = ${CC} ${DEPFLAGS} $(INC_FLAGS) ${CFLAGS} ${CPPFLAGS} -c -o $@ COMPILE.cc = ${CXX} ${DEPFLAGS} $(INC_FLAGS) ${CXXFLAGS} ${CPPFLAGS} -c -o $@ LINK.o = ${LD} ${LDFLAGS} ${LDLIBS} # precompile step PRECOMPILE = # postcompile step POSTCOMPILE = mv -f ${DEPDIR}/$*.Td ${DEPDIR}/$*.d all: ${BINDIR}/${EXEFILE} @echo "+++++ Generating executable $^..." ${BINDIR}/${EXEFILE} : ${OBJS} @echo "+++++ Linking $@ from $^..." ${LINK.o} -o $@ $^ ${BUILDDIR}/%.o: ${SRCDIR}/%.cpp @echo "+++++ Target $@ fired with deps $^..." ${PRECOMPILE} ${COMPILE.cc} $< ${POSTCOMPILE} .PRECIOUS = ${DEPDIR}/%.d ${DEPDIR}/%.d : ${SRCDIR}/.cpp @echo "+++++ Target $@ fired with deps $^..." ${PRECOMPILE} ${COMPILE.cc} $^ ${POSTCOMPILE} .PHONY : clean clean : ${RM} -r ${BUILDDIR} ${DEPDIR} .PHONY : distclean distclean: clean ${RM} ${EXEFILE} ${DISTOUTPUT} .PHONY : install install: mkdir -p ${DEPDIR} ${BUILDDIR} .PHONY: uninstall uninstall: @echo no uninstall tasks configured .PHONY: check check: @echo no tests configured dist : ${DISTFILES} ${TAR} -cvzf ${DISTOUTPUT} $^ .PHONY: help help: @echo available targets: all dist clean distclean install uninstall check # Let's include dependencies calculated by CC -include $(addprefix ${DEPDIR}/,${DEPS}) |
Como veréis gran parte de la magia está en el directorio .d donde el compilador genera una serie de ficheros de dependencias que luego son incluidos (mirad la última línea del fichero Makefile) como dependencias también.
Os sugiero que reviséis con especial atención la documentación sobre make que os he dejado más arriba para que así podáis comprender mejor cómo funciona este Makefile.