From a4419536621943012477a48fc1fc5b1c730a2834 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 17 Sep 2009 10:30:56 -0300 Subject: [PATCH] Initial import of the all mighty make based build system This is a test/example of a nice make based build system that support automatic dependencies, variants, separated build directory, automatic generation of pre-compiled header, subproject embedding and some other goodies. The only important file is Lib.mak, all the rest is just an usage example and/or test. --- .gitignore | 1 + Build.mak | 15 +++ Config.mak | 10 ++ Lib.mak | 243 +++++++++++++++++++++++++++++++++++++++++++ Makefile | 10 ++ README | 27 +++++ Toplevel.mak | 15 +++ lib1/Build.mak | 7 ++ lib1/Makefile | 10 ++ lib1/lib1.c | 9 ++ lib1/lib1.h | 11 ++ lib2/Build.mak | 6 ++ lib2/Makefile | 10 ++ lib2/lib2.cpp | 15 +++ lib2/lib2.h | 3 + prog/Build.mak | 8 ++ prog/Makefile | 10 ++ prog/main.cpp | 15 +++ subproj/Build.mak | 9 ++ subproj/Config.mak | 10 ++ subproj/Lib.mak | 1 + subproj/Makefile | 10 ++ subproj/Toplevel.mak | 18 ++++ subproj/subproj.c | 9 ++ subproj/subproj.h | 11 ++ 25 files changed, 493 insertions(+) create mode 100644 .gitignore create mode 100644 Build.mak create mode 100644 Config.mak create mode 100644 Lib.mak create mode 100644 Makefile create mode 100644 README create mode 100644 Toplevel.mak create mode 100644 lib1/Build.mak create mode 100644 lib1/Makefile create mode 100644 lib1/lib1.c create mode 100644 lib1/lib1.h create mode 100644 lib2/Build.mak create mode 100644 lib2/Makefile create mode 100644 lib2/lib2.cpp create mode 100644 lib2/lib2.h create mode 100644 prog/Build.mak create mode 100644 prog/Makefile create mode 100644 prog/main.cpp create mode 100644 subproj/Build.mak create mode 100644 subproj/Config.mak create mode 120000 subproj/Lib.mak create mode 100644 subproj/Makefile create mode 100644 subproj/Toplevel.mak create mode 100644 subproj/subproj.c create mode 100644 subproj/subproj.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/Build.mak b/Build.mak new file mode 100644 index 0000000..0f18c0b --- /dev/null +++ b/Build.mak @@ -0,0 +1,15 @@ + +# Include sub-directories makefiles + +C := subproj +include $T/subproj/Build.mak + +C := lib1 +include $T/lib1/Build.mak + +C := lib2 +include $T/lib2/Build.mak + +C := prog +include $T/prog/Build.mak + diff --git a/Config.mak b/Config.mak new file mode 100644 index 0000000..e76a8a9 --- /dev/null +++ b/Config.mak @@ -0,0 +1,10 @@ + +# Use debug flavor by default +F := dbg + +# C compiler +CC := gcc + +# Use precompiled headers +GCH := 1 + diff --git a/Lib.mak b/Lib.mak new file mode 100644 index 0000000..259a659 --- /dev/null +++ b/Lib.mak @@ -0,0 +1,243 @@ + +ifndef Lib.mak.included +Lib.mak.included := 1 + +# These variables should be provided by the includer Makefile: +# P should be the project name, mostly used to handle include directories +# T should be the path to the top-level directory. +# C should be the path to the current directory. + +# Load top-level directory local configuration +sinclude $T/Config.mak + +# Verbosity flag (empty show nice messages, 1 be verbose) +# honour make -s flag +override V := $(if $(findstring s,$(MAKEFLAGS)),1,$V) + +# Flavor (variant), should be one of "dbg", "opt" or "cov" +F ?= opt + +# Use C++ linker by default +LINKER := $(CXX) + +# Use precompiled headers if non-empty +GCH ?= + + +# Directories +############## + +# Use absolute paths to avoid problems with automatic dependencies when +# building from subdirectories +T := $(abspath $T) + +# Name of the current directory, relative to $T +R := $(subst $T,,$(patsubst $T/%,%,$(CURDIR))) + +# Base directory where to put variants +D ?= $T/build + +# Generated files top directory +G ?= $D/$F + +# Objects (and other garbage like precompiled headers and dependency files) +# directory +O ?= $G/obj + +# Binaries directory +B ?= $G/bin + +# Libraries directory +L ?= $G/lib + +# Includes directory +I ?= $G/include + +# Generated includes directory +J ?= $G/geninc + + +# Functions +############ + +# Find sources files and get the corresponding object names +# The first argument should be the sources extension ("c" or "cpp" typically) +# It expects the variable $T and $O to be defined as commented previously in +# this file. $C should be defined to the path to the current directory relative +# to the top-level. +find_objects = $(patsubst $T/%.$1,$O/%.o,$(shell find $T/$C -name '*.$1')) + +# Abbreviate a file name. Cut the leading part of a file if it match to the $T +# directory, so it can be displayed as if it were a relative directory. Take +# just one argument, the file name. +abbr = $(addprefix $(shell echo $R | sed 's|/\?\([^/]\+\)/\?|../|g'),\ + $(subst $T,.,$(patsubst $T/%,%,$1))) + +# Execute a command printing a nice message if $V is empty +# The first argument is mandatory and it's the command to execute. The second +# and third arguments are optional and are the target name and command name to +# pretty print. +vexec = $(if $V,,\ + echo ' $(notdir $(if $3,$(strip $3),$(firstword $1))) \ + $(call abbr,$(if $2,$(strip $2),$@))' ; )$1 + +# Same as vexec but it silence the echo command (prepending a @). +exec = $(if $V,,@)$(call vexec,$1,$2,$3) + +# Compile a source file to an object, generating pre-compiled headers and +# dependencies. The pre-compiled headers are generated only if the system +# includes change. This function is designed to be used as a command in a rule. +# It takes one argument only, the type of file to compile (typically "c" or +# "cpp"). What to compile and the output files are built using the automatic +# variables from a rule. +define compile +$(if $(GCH),\ +@if test -f $O/$*.d; then \ + tmp=`mktemp`; \ + h=`awk -F: '!$$0 {f = 1} $$0 && f {print $$1}' $O/$*.d`; \ + grep -h '^#include <' $(call abbr,$<) $$h | sort -u > "$$tmp"; \ + if diff -q -w "$(call abbr,$O/$*.$1.h)" "$$tmp" > /dev/null 2>&1; \ + then \ + rm "$$tmp"; \ + else \ + mv "$$tmp" "$(call abbr,$O/$*.$1.h)"; \ + $(call vexec,$(COMPILE.$1) -o "$O/$*.$1.h.gch" "$O/$*.$1.h",\ + $O/$*.$1.h.gch); \ + fi \ +else \ + touch "$(call abbr,$O/$*.$1.h)"; \ +fi \ +) +$(call exec,$(COMPILE.$1) -o $@ -MMD -MP $(if $(GCH),-include $O/$*.$1.h) $<) +endef + +# Link object files to build an executable. The objects files are taken from +# the prerequisite files ($O/%.o). If in the prerequisite files are shared +# objects ($L/lib%.so), they are included as libraries to link to (-l%). This +# function is designed to be used as a command in a rule. The ouput name is +# taken from the rule automatic variables. If an argument is provided, it's +# included in the link command line. The variable LINKER is used to link the +# executable; for example, if you want to link a C++ executable, you should use +# LINKER := $(CXX). +link = $(call exec,$(LINKER) $(LDFLAGS) $(TARGET_ARCH) -o $@ $1 \ + $(patsubst $L/lib%.so,-l%,$(filter %.so,$^)) \ + $(foreach obj,$(filter %.o,$^),$(obj))) + + +# Overrided flags +################## + +# Warn about everything +override CPPFLAGS += -Wall + +# Use the includes directories to search for includes +override CPPFLAGS += -I$I -I$J + +# Be standard compilant +override CFLAGS += -std=c99 -pedantic +override CXXFLAGS += -std=c++98 -pedantic + +# Use the generated library directory to for libraries +override LDFLAGS += -L$L -Wall + +# Make sure the generated libraries can be found +export LD_LIBRARY_PATH := $L:$(LD_LIBRARY_PATH) + + +# Variant flags +################ + +ifeq ($F,dbg) +override CPPFLAGS += -ggdb -DDEBUG +endif + +ifeq ($F,opt) +override CPPFLAGS += -O2 -DNDEBUG +endif + +ifeq ($F,cov) +override CPPFLAGS += -ggdb -pg --coverage +override LDFLAGS += -pg --coverage +endif + + +# Automatic rebuilding when flags or commands changes +###################################################### + +# Re-compile C files if one of this variables changes +COMPILE.c.FLAGS := $(CC) ~ $(CPPFLAGS) ~ $(CFLAGS) ~ $(TARGET_ARCH) + +# Re-compile C++ files if one of this variables changes +COMPILE.cpp.FLAGS := $(CXX) ~ $(CPPFLAGS) ~ $(CXXFLAGS) ~ $(TARGET_ARCH) + +# Re-link binaries and libraries if one of this variables changes +LINK.o.FLAGS := $(LD) ~ $(LDFLAGS) ~ $(TARGET_ARCH) + + +# Default rules +################ + +$O/%.o: $T/%.c $G/compile-c-flags + $(call compile,c) + +$O/%.o: $T/%.cpp $G/compile-cpp-flags + $(call compile,cpp) + +$B/%: $G/link-o-flags + $(call link) + +$L/%.so: override CFLAGS += -fPIC +$L/%.so: override CXXFLAGS += -fPIC +$L/%.so: $G/link-o-flags + $(call link,-shared) + +.PHONY: clean +clean: + $(call exec,$(RM) -r $D,$D) + + +# Automatic dependency handling +################################ + +sinclude $(shell test -d $O && find $O -name '*.d') + + +# Create build directory structure +################################### + +# Create a file with flags used to trigger rebuilding when they change. The +# first argument is the name of the file where to store the flags, the second +# are the flags and the third argument is a text to be displayed if the flags +# have changed. This should be used as a rule action or something where +# a shell script is expected. +gen_rebuild_flags = if test x"$2" != x"`cat $1 2>/dev/null`"; then \ + test -f $1 && echo "$3"; \ + echo "$2" > $1 ; fi + +# Create $O, $B, $L, $I and $J directories and replicate the directory +# structure of the project into $O. Create one symlink "last" to the current +# build directory and another to use as include directory. It update the flags +# files to detect flag and/or compiler changes to force a rebuild. +# +# NOTE: the second mkdir can yield no arguments if the project don't have any +# subdirectories, that's why the current directory "." is included, so it +# won't show an error message in case of no subdirectories. +setup_build_dir__ := $(shell \ + mkdir -p $O $B $L $I $J; \ + mkdir -p . $(addprefix $O,$(patsubst $T%,%,\ + $(shell find $T -type d -not -path '$D*'))); \ + $(call gen_rebuild_flags,$G/compile-c-flags, \ + $(COMPILE.c.FLAGS),C compiler or flags;); \ + $(call gen_rebuild_flags,$G/compile-cpp-flags, \ + $(COMPILE.cpp.FLAGS),C++ compiler or flags;); \ + $(call gen_rebuild_flags,$G/link-o-flags, \ + $(LINK.o.FLAGS),linker or link flags;); \ + test -L $I/$P || ln -s $T $I/$P; \ + test -L $D/last || ln -s $F $D/last ) + +# Print any generated message (if verbose) +$(if $V,,$(if $(setup_build_dir__), \ + $(info !! Something changed: $(setup_build_dir__) \ + re-building affected files...))) + +endif diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f78a4f0 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ + +# Top-level directory +T := . + +# Default goal for building this directory +.DEFAULT_GOAL := all + +# Include the top-level makefile +include $T/Toplevel.mak + diff --git a/README b/README new file mode 100644 index 0000000..083dea7 --- /dev/null +++ b/README @@ -0,0 +1,27 @@ + +This is a test/example for a nice Make based build system. + +The Config.mak should not be saved to the repositories usually, but in this +case it is because part of this demonstration is to show how to customize the +build system through Config.mak, and specially to show how a project can be +"embedded" into another tweaking the Config.mak. + +lib1 is a standalone C library compiled into a shared object. lib2 is another +shared library which uses lib1 and subproj, which is a standalone project +"embedded" into this one. subproj produces another standalone shared object. +Finally, prog is a program which uses lib1 and lib2. + +Every project have it's copy of Lib.mak and it's own Toplevel.mak, which has +some global permanent configuration (which doesn't depends on users taste, it +just have information about the project, like it's name, and does the work to +include Lib.mak, etc.). Lib.mak shouldn't be modified ever (unless you're +hacking the build system) and Toplevel.mak should be changes very rarely. + +Then each directory containing some library or program to build (or directories +to include) has a Build.mak, which has only the logic to build the +programs/libraries. A well-known Makefile is added to each directory where you +want to be able to do "make", just for convenience. This Makefile should be +created once, with the default target to build and path to the top-level +directory and never touched again. Build.mak should be changes only to add new +programs or libraries to build. + diff --git a/Toplevel.mak b/Toplevel.mak new file mode 100644 index 0000000..bcf2b56 --- /dev/null +++ b/Toplevel.mak @@ -0,0 +1,15 @@ + +# Project name +P := remake + +# Include the build system library +include $T/Lib.mak + +# Include the Build.mak for this directory +include $T/Build.mak + +# Phony rule to make all the targets (sub-makefiles can append targets to build +# to the $(all) variable). +.PHONY: all +all: $(all) + diff --git a/lib1/Build.mak b/lib1/Build.mak new file mode 100644 index 0000000..7661dc4 --- /dev/null +++ b/lib1/Build.mak @@ -0,0 +1,7 @@ + +$L/liblib1.so: LINKER := $(CC) +$L/liblib1.so: $(call find_objects,c) + +.PHONY: lib1 +lib1: $L/liblib1.so + diff --git a/lib1/Makefile b/lib1/Makefile new file mode 100644 index 0000000..fe10532 --- /dev/null +++ b/lib1/Makefile @@ -0,0 +1,10 @@ + +# Top-level directory +T := .. + +# Include the top-level build +include $T/Toplevel.mak + +# Default goal for building this directory +.DEFAULT_GOAL := $L/liblib1.so + diff --git a/lib1/lib1.c b/lib1/lib1.c new file mode 100644 index 0000000..e7212f4 --- /dev/null +++ b/lib1/lib1.c @@ -0,0 +1,9 @@ + +#include "lib1.h" +#include + +void lib1(void) +{ + printf("lib1()\n"); +} + diff --git a/lib1/lib1.h b/lib1/lib1.h new file mode 100644 index 0000000..2aafe60 --- /dev/null +++ b/lib1/lib1.h @@ -0,0 +1,11 @@ + +#ifdef __cplusplus +extern "C" { +#endif + +void lib1(void); + +#ifdef __cplusplus +} +#endif + diff --git a/lib2/Build.mak b/lib2/Build.mak new file mode 100644 index 0000000..02841c5 --- /dev/null +++ b/lib2/Build.mak @@ -0,0 +1,6 @@ + +$L/liblib2.so: $(call find_objects,cpp) $L/liblib1.so $L/libsubproj.so + +.PHONY: lib2 +lib2: $L/liblib2.so + diff --git a/lib2/Makefile b/lib2/Makefile new file mode 100644 index 0000000..99f8aad --- /dev/null +++ b/lib2/Makefile @@ -0,0 +1,10 @@ + +# Top-level directory +T := .. + +# Include the top-level build +include $T/Toplevel.mak + +# Default goal for building this directory +.DEFAULT_GOAL := $L/liblib2.so + diff --git a/lib2/lib2.cpp b/lib2/lib2.cpp new file mode 100644 index 0000000..d5c07c4 --- /dev/null +++ b/lib2/lib2.cpp @@ -0,0 +1,15 @@ + +#include "lib2.h" + +#include +#include + +#include + +void lib2(void) +{ + printf("lib2()\n"); + lib1(); + subproj(); +} + diff --git a/lib2/lib2.h b/lib2/lib2.h new file mode 100644 index 0000000..80dab51 --- /dev/null +++ b/lib2/lib2.h @@ -0,0 +1,3 @@ + +void lib2(void); + diff --git a/prog/Build.mak b/prog/Build.mak new file mode 100644 index 0000000..d3a59ed --- /dev/null +++ b/prog/Build.mak @@ -0,0 +1,8 @@ + +$B/prog: $(call find_objects,cpp) $L/liblib1.so $L/liblib2.so + +.PHONY: prog +prog: $B/prog + +all += prog + diff --git a/prog/Makefile b/prog/Makefile new file mode 100644 index 0000000..717a34e --- /dev/null +++ b/prog/Makefile @@ -0,0 +1,10 @@ + +# Top-level directory +T := .. + +# Default goal for building this directory +.DEFAULT_GOAL := prog + +# Include the top-level build +include $T/Toplevel.mak + diff --git a/prog/main.cpp b/prog/main.cpp new file mode 100644 index 0000000..19ec21f --- /dev/null +++ b/prog/main.cpp @@ -0,0 +1,15 @@ + +#include +#include + +#include + +int main() +{ + std::cout << "prog:main() start\n"; + lib1(); + lib2(); + std::cout << "prog:main() end\n"; + return 0; +} + diff --git a/subproj/Build.mak b/subproj/Build.mak new file mode 100644 index 0000000..01d8779 --- /dev/null +++ b/subproj/Build.mak @@ -0,0 +1,9 @@ + +$L/libsubproj.so: LINKER := $(CC) +$L/libsubproj.so: $(call find_objects,c) + +.PHONY: subproj +subproj: $L/libsubproj.so + +all += subproj + diff --git a/subproj/Config.mak b/subproj/Config.mak new file mode 100644 index 0000000..d7624aa --- /dev/null +++ b/subproj/Config.mak @@ -0,0 +1,10 @@ + +# Use the container project top-level directory as ours +T := .. + +# Include the "parent" project config +sinclude $T/Toplevel.mak + +# Include the "parent" project config +.DEFAULT_GOAL := subproj + diff --git a/subproj/Lib.mak b/subproj/Lib.mak new file mode 120000 index 0000000..f07a38e --- /dev/null +++ b/subproj/Lib.mak @@ -0,0 +1 @@ +../Lib.mak \ No newline at end of file diff --git a/subproj/Makefile b/subproj/Makefile new file mode 100644 index 0000000..f78a4f0 --- /dev/null +++ b/subproj/Makefile @@ -0,0 +1,10 @@ + +# Top-level directory +T := . + +# Default goal for building this directory +.DEFAULT_GOAL := all + +# Include the top-level makefile +include $T/Toplevel.mak + diff --git a/subproj/Toplevel.mak b/subproj/Toplevel.mak new file mode 100644 index 0000000..4738f47 --- /dev/null +++ b/subproj/Toplevel.mak @@ -0,0 +1,18 @@ + +# Project name +P := subproj + +# Load top-level directory local configuration +sinclude $T/Config.mak + +# Include the build system library +include $T/Lib.mak + +# Include the Build.mak for this directory +include $T/Build.mak + +# Phony rule to make all the targets (sub-makefiles can append targets to build +# to the $(all) variable). +.PHONY: all +all: $(all) + diff --git a/subproj/subproj.c b/subproj/subproj.c new file mode 100644 index 0000000..2188e52 --- /dev/null +++ b/subproj/subproj.c @@ -0,0 +1,9 @@ + +#include "subproj.h" +#include + +void subproj(void) +{ + printf("subproj()\n"); +} + diff --git a/subproj/subproj.h b/subproj/subproj.h new file mode 100644 index 0000000..d29126c --- /dev/null +++ b/subproj/subproj.h @@ -0,0 +1,11 @@ + +#ifdef __cplusplus +extern "C" { +#endif + +void subproj(void); + +#ifdef __cplusplus +} +#endif + -- 2.43.0