Building multiple executables with similar rules - c++

I am writing something like an interactive tutorial for C++. The tutorial will consist of two parts: one is compiled into a library (I'm using Scons to build that), and the other (the lessons) is shipped with the tutorial to be compiled by the end user. I'm currently looking for a good, easy way for people to build these lessons.
Basically, the second part is a directory with all the lessons in it, each in its own directory. Each lesson will have at least a lesson.cpp and a main.cpp file, there may be also other files, the existence of which I will not know until after it is shipped -- the end user will create these. It will look something like this:
all_lessons/
helloworld/
lesson.cpp
main.cpp
even_or_odd/
lesson.cpp
main.cpp
calculator/
lesson.cpp
main.cpp
user_created_add.cpp
Each of these will need to be compiled according to almost the same rules, and the command for compiling should be possible to run from one of the lesson directories (helloworld/, etc.).
Seeing as the rest of the project is built with Scons, it would make sense to use it for this part, too. However, Scons searches for the SConstruct file in the directory it is run from: would it be acceptable to put a SConstruct file in each lesson directory, plus a SConscript in the all_lessons/ directory that gives the general rules? This seems to go against the typical way Scons expects projects to be organised: what are the potential pitfalls of this approach? Could I put a SConstruct file instead of the SConscript one, and thereby make it possible to build from either directory (using exports to avoid endless recursion, I'm guessing)?
Also, I may at some point want to replace the lesson.cpp with a lesson.py that generates the necessary files; will Scons allow me to do this easily with builders, or is there a framework that would be more convenient?
In the end, I want to end up with the following (or equivalent with different build systems):
all_lessons/
SConstruct
helloworld/
SConstruct
lesson.cpp
main.cpp
even_or_odd/
SConstruct
lesson.py
main.cpp
calculator/
SConstruct
lesson.cpp
main.cpp
user_created_add.cpp
Running scons all in the all_lessons directory would need to:
Run even_or_odd/lesson.py to generate even_or_odd/lesson.cpp.
Realise that user_created_add.cpp also needs to be compiled.
Produce an executable for each lesson.
Running scons in even_or_odd/, or scons even_or_odd in all_lessons/ should produce an executable identical to the one above (same compile flags).
Summary:
Is Scons suitable for/capable of this?
Does Scons work well when SConscript files are above SConstruct files?
Does Scons work well with multiple SConstrcut files for one project, SConscripting each other?
Is the Scons builder system suitable for using Python scripts to generate C++ files?
Is there any advantage of using a different build system/writing my own build framework that I'm missing?
Any further comments are, of course, welcome.
Thanks.

You can actually do this with a few lines of GNU Make.
Below are two makefiles that allow building and cleaning from all_lessons directory and individual project directories. It assumes that all C++ sources in that directory comprise an executable file which gets named after its directory. When building and cleaning from the top level source directory (all_lessons) it builds and cleans all the projects. When building and cleaning from a project's directory it only builds and cleans the project's binaries.
These makefiles also automatically generate dependencies and are fully parallelizable (make -j friendly).
For the following example I used the same source file structure as you have:
$ find all_lessons
all_lessons
all_lessons/even_or_odd
all_lessons/even_or_odd/main.cpp
all_lessons/Makefile
all_lessons/helloworld
all_lessons/helloworld/lesson.cpp
all_lessons/helloworld/main.cpp
all_lessons/project.mk
all_lessons/calculator
all_lessons/calculator/lesson.cpp
all_lessons/calculator/user_created_add.cpp
all_lessons/calculator/main.cpp
To be able to build from individial project directories project.mk must be symlinked as project/Makefile first
[all_lessons]$ cd all_lessons/calculator/
[calculator]$ ln -s ../project.mk Makefile
[helloworld]$ cd ../helloworld/
[helloworld]$ ln -s ../project.mk Makefile
[even_or_odd]$ cd ../even_or_odd/
[even_or_odd]$ ln -s ../project.mk Makefile
Let's build one project:
[even_or_odd]$ make
make -C .. project_dirs=even_or_odd all
make[1]: Entering directory `/home/max/src/all_lessons'
g++ -c -o even_or_odd/main.o -Wall -Wextra -MD -MP -MF even_or_odd/main.d even_or_odd/main.cpp
g++ -o even_or_odd/even_or_odd even_or_odd/main.o
make[1]: Leaving directory `/home/max/src/all_lessons'
[even_or_odd]$ ./even_or_odd
hello, even_or_odd
Now build all projects:
[even_or_odd]$ cd ..
[all_lessons]$ make
g++ -c -o calculator/lesson.o -Wall -Wextra -MD -MP -MF calculator/lesson.d calculator/lesson.cpp
g++ -c -o calculator/user_created_add.o -Wall -Wextra -MD -MP -MF calculator/user_created_add.d calculator/user_created_add.cpp
g++ -c -o calculator/main.o -Wall -Wextra -MD -MP -MF calculator/main.d calculator/main.cpp
g++ -o calculator/calculator calculator/lesson.o calculator/user_created_add.o calculator/main.o
g++ -c -o helloworld/lesson.o -Wall -Wextra -MD -MP -MF helloworld/lesson.d helloworld/lesson.cpp
g++ -c -o helloworld/main.o -Wall -Wextra -MD -MP -MF helloworld/main.d helloworld/main.cpp
g++ -o helloworld/helloworld helloworld/lesson.o helloworld/main.o
[all_lessons]$ calculator/calculator
hello, calculator
[all_lessons]$ helloworld/helloworld
hello, world
Clean one project:
[all_lessons]$ cd helloworld/
[helloworld]$ make clean
make -C .. project_dirs=helloworld clean
make[1]: Entering directory `/home/max/src/all_lessons'
rm -f helloworld/lesson.o helloworld/main.o helloworld/main.d helloworld/lesson.d helloworld/helloworld
make[1]: Leaving directory `/home/max/src/all_lessons'
Clean all projects:
[helloworld]$ cd ..
[all_lessons]$ make clean
rm -f calculator/lesson.o calculator/user_created_add.o calculator/main.o even_or_odd/main.o helloworld/lesson.o helloworld/main.o calculator/user_created_add.d calculator/main.d calculator/lesson.d even_or_odd/main.d calculator/calculator even_or_odd/even_or_odd helloworld/helloworld
The makefiles:
[all_lessons]$ cat project.mk
all :
% : forward_ # build any target by forwarding to the main makefile
$(MAKE) -C .. project_dirs=$(notdir ${CURDIR}) $#
.PHONY : forward_
[all_lessons]$ cat Makefile
# one directory per project, one executable per directory
project_dirs := $(shell find * -maxdepth 0 -type d )
# executables are named after its directory and go into the same directory
exes := $(foreach dir,${project_dirs},${dir}/${dir})
all : ${exes}
# the rules
.SECONDEXPANSION:
objects = $(patsubst %.cpp,%.o,$(wildcard $(dir ${1})*.cpp))
# link
${exes} : % : $$(call objects,$$*) Makefile
g++ -o $# $(filter-out Makefile,$^) ${LDFLAGS} ${LDLIBS}
# compile .o and generate dependencies
%.o : %.cpp Makefile
g++ -c -o $# -Wall -Wextra ${CPPFLAGS} ${CXXFLAGS} -MD -MP -MF ${#:.o=.d} $<
.PHONY: clean
clean :
rm -f $(foreach exe,${exes},$(call objects,${exe})) $(foreach dir,${project_dirs},$(wildcard ${dir}/*.d)) ${exes}
# include auto-generated dependency files
-include $(foreach dir,${project_dirs},$(wildcard ${dir}/*.d))

As an exercise in learning scons, I've tried to answer your question. Unfortunately, I'm no expert, so I can't tell you what's the best/ideal way, but here's a way that works.
Scons is suitable for/capable of this. (This is exactly what build tools are for.)
Not applicable. (I don't know.)
Scons seems to work well with multiple SConstrcut files for one project, SConscripting each other.
The Scons builder system can use Python scripts to generate C++ files.
A different build system? To each his own.
Using the hierarchy you defined, there's a SConstruct file in each folder. You can run scons in a subfolder to build that project or at the top level to build all projects (not sure how you'd alias "all" to the default build). You can run scons -c to clean the project and scons automatically figures out which files it created and cleans them (including the generated lesson.cpp).
However, if you want compiler flags to propagate from the top-level file down, I think it's better to use SConscript files -- except I'm not sure about making these compile on their own.
./SConstruct
env = Environment()
env.SConscript(dirs=['calculator', 'even_or_odd', 'helloworld'], name='SConstruct')
./calculator/SConstruct and ./calculator/helloworld
env = Environment()
env.Program('program', Glob('*.cpp'))
./even_or_odd/SConstruct
env = Environment()
def add_compiler_builder(env):
# filename transformation
suffix = '.cpp'
src_suffix = '.py'
# define the build method
rule = 'python $SOURCE $TARGET'
bld = Builder(action = rule,
suffix = suffix,
src_suffix = src_suffix)
env.Append(BUILDERS = {'Lesson' : bld})
return env
add_compiler_builder(env)
env.Lesson('lesson.py')
env.Program('program', Glob('*.cpp'))
Using SConscripts
I convert the subfolder's SConstructs to SConscripts and can lift the code build specifics out of the subfolders, but then you need to run scons -u to build in a subfolder (to search upwards for the root SConstruct).
./SConstruct
def default_build(env):
env.Program('program', Glob('*.cpp'))
env = Environment()
env.default_build = default_build
Export('env')
env.SConscript(dirs=['calculator', 'even_or_odd', 'helloworld'])
./helloworld/SConscript, etc...
Import('env')
env.default_build(env)

Is it essential that the command for compiling be run from the lesson directory? If not then I would personally create all_lessons/makefile with the following contents:
lessons = helloworld even_or_odd calculator
all: $(lessons)
# for each $lesson, the target is $lesson/main built from $lesson/main.cpp and $lesson/lesson.cpp
# NB: the leading space on the second line *must* be a tab character
$(lessons:%=%/main): %/main: %/main.cpp %/lesson.cpp
g++ -W -Wall $+ -o $#
All lessons could then be built with "make" or "make all" in the all_lessons directory, or a specific lesson with e.g. "make helloworld/main".

As far as I have found, this is the best solution available:
The directory is structured in the same way, but instead of having multiple SConstruct files, the lessons have a SConscript file each, where defaults are overridden as necessary. The SConstruct files are generated by an external script as necessary, and SCons is invoked.
An overview:
all_lessons/
helloworld/
SConscript
lesson.cpp
main.cpp
even_or_odd/
SConscript
lesson.py
main.cpp
calculator/
SConscript
lesson.cpp
main.cpp
user_created_add.cpp
Using Glob, the SConscript file can make all files with the cpp extension be compiled. It can also use a builder (either one invoking a simple command, or a fully-fledged one) that will generate the lesson, meaning it's possible to even just store the lesson as metadata and have it generated on the spot.
So, to answer the questions:
Yes.
I don't know, but it is not required for the purpose of this.
As far as I have seen, no (issues with paths relative to SConstruct, amongst other things).
Yes, with several options being available.
I don't know.
Downsides to the suggested approach: this does require making a meta-build system separately. The number of files where options can be specified is higher, and the SConscript files give a lot of room for error.

Here is my way.
# SConstruct or SConscript
def getSubdirs(dir) :
lst = [ name for name in os.listdir(dir) if os.path.isdir(os.path.join(dir, name)) and name[0] != '.' ]
return lst
env = Environment()
path_to_lessons = '' # path to lessons
# configure your environment, set common rules and parameters for all lessons
for lesson in getSubdirs(path_to_lessons) :
lessonEnv = env.Clone()
# configure specific lesson, for example i'h ve checked SConscript file in lesson dir
# and if it exist, execute it with lessonEnv and append env specific settings
if File(os.path.join(path_to_lessons, lesson, 'lesson.scons')).exists() :
SConscript(os.path.join(lesson, 'lesson.scons', export = ['lessonEnv'])
# add lesson directory to include path
lessonEnv.Append(CPPPATH = os.path.join(path_to_lessons, lesson));
lessonEnv.Program(lesson, Glob(os.path.join(path_to_lessons, lesson, '*.cpp'))
Now you have :
env - core Environment that contain common rules and parameters for
all lessons
lessonEnv - clone of core env, but if you have
lesson.scons in specific lesson dir, you can additional configure
that environment or rewrite some parameters.

Related

Compiling larger C++ projects in VSCode

Im trying to compile a C++ project using MinGW and can compile a simple main.cpp file with hello world without problems using g++ main.cpp -o main and also with external libraries using main.cpp extlib.cpp -o main.
But say im working on a rather large project with 10s of .cpp files organised inside of different files, how can I get the compiler to find all the cpp files that are needed? I know i can use main.cpp libs/*.cpp -o main but this will only compile all the source files inside of libs but not inside folders in libs.
Ive looked into make and cmake but dont understand how those automate the process if you still have to manually enter the directories. Is there no way to simply hit compile or at least a command line command to compile all the needed files inside a directory? This seems to work with #include without issues?
If you want to stick with MinGW and GNU Make I would probably use a Makefile that looks something like this to start with. You basically only need to maintain the srcs-variable by adding your source-files there. Usually you can use the wildcard-function for this if you have sub dirs. The rest of the Makefile (which can be left alone) sets up a build of an executable main.exe that depends on all the object-files. I also included dependency-handling via the deps-variable and the compiler flag -MMD which comes in handy when the project grows.
srcs := $(wildcard *.cpp) $(wildcard dir1/*.cpp) $(wildcard dir2/*.cpp)
objs := $(srcs:.cpp=.o)
deps := $(objs:.o=.d)
app := main.exe
CXXFLAGS := -MMD -Og -g -Wall -Werror -Wpedantic -std=c++2a
$(app): $(objs)
$(CXX) $(LDFLAGS) -o $# $^ $(LDLIBS)
-include $(deps)
.PHONY: clean
clean:
rm -f $(objs) $(deps)
I use CMake for simple projects.
Here's the simplest example I came with (CMakeLists.txt to put along your main.cpp in the root of your project):
cmake_minimum_required(VERSION 3.1)
SET(CMAKE_APP_NAME "Project")
project (${CMAKE_APP_NAME})
# list here your directories
INCLUDE_DIRECTORIES(dir1)
INCLUDE_DIRECTORIES(dir2)
# add an executable and list all files to compile
add_executable(${CMAKE_APP_NAME} main.cpp
dir1/file1.cpp
dir1/file1.h
dir2/file2.h
dir2/file2.cpp
)
Once your project becomes more complex, you could use file(GLOB*) to avoid writing all the files.
Overall, the most "automated" way to build a larger project is to use CMake. Keep learning it. You can use file(GLOB) to avoid listing every file in CMakeLists.txt. This is not recommended (see discussion here), but I do it anyway and never had any issues.

Writing scons script to handle compilation of files in many subdirectories

I've got a project which consists of many files in subdirectories. I have a simple Makefile which handles the compilation. It looks like this:
CC = g++ -Wall -ansi -pedantic
all:
$(CC) -O2 engine/core/*.cpp engine/objects3d/*.cpp engine/display/*.cpp engine/io /*.cpp engine/math/*.cpp engine/messages/*.cpp *.cpp -o project1 -lGL -lGLU -lX11 `sdl-config --cflags --libs`
clean:
#echo Cleaning up...
#rm project1
#echo Done.
However I need to migrate to SCons. I have no idea how to write a script that would automatically handle finding all the *.cpp files in subdirectories and included them in the compilation process.
This is how to do what you have in your Makefile in SCons. You should put this Python code in a file at the root of the project called SConstruct, and just simply execute scons. To clean, execute scons -c
env = Environment()
env.Append(CPPFLAGS=['-Wall', '-ansi', '-pedantic', '-O2', '-lGL', '-lGLU', '-lX11'])
# Determine compiler and linker flags for SDL
env.ParseConfig('sdl-config --cflags')
env.ParseConfig('sdl-config --libs')
# Remember that the SCons Glob() function is not recursive
env.Program(target='project1',
source=[Glob('engine/core/*.cpp'),
Glob('engine/objects3d/*.cpp'),
Glob('engine/display/*.cpp)',
Glob('engine/io/*.cpp'),
Glob('engine/math/*.cpp'),
Glob('engine/messages/*.cpp'),
Glob('*.cpp')])
Here is a link for using SDL with SCons.
And here is info on the SCons ParseConfig() function.

Implicit Build Rules: GNU Make Multiple Makefiles Multiple Directories

I have the following directory structure:
.
..
./Graphic/
./Graphic/SymbolXLib
There are several other directories in this project but I won't list them for simplicities sake.I want a main makefile that drives the build of other Makefiles stored in their own directories. There are several project comming together, so I can't just move source around.
The main makefile is defined as:
[mehoggan#hogganz400 Core]$ cat ./Makefile
CORE_LIBS_DIR = libs
OBJS_DIR = obj/symb_obj
include ./Graphic/SymbolXLib/Makefile
The Graphic makefile is defined as:
#
# make BUILD_MODE={release|debug} OS_ARCH={32|64}
#
# default is 32-bit release build
#
BUILD_MODE = release
OS_ARCH = 64
OBJS_DIR = $(BUILD_MODE)$(OS_ARCH)
SRC = \
./Graphic/SymbolXLib/CartoCursor.cpp \
...
./Graphic/SymbolXLib/TextureConversion.cpp \
$(NULL)
CC = gcc -fPIC
OBJS = $(SRC:%.cpp=$(OBJS_DIR)/%.o)
COPTS = -m$(OS_ARCH) -O2
CDEFS = -DLINUXx86 \
-I../../../SharedArcGIS/Include/GraphicsPipeline/Display/SymbolX/SymbolXLib \
-I../../../SharedArcGIS/Include/System/Geometry/GeometryXLib \
-I../../../ArcSDE/pe/include \
-I../../../ArcSDE/shape/include
CFLAGS = $(COPTS) $(CDEFS) $(CINCS)
TARGET = libSymbolXLib.a
all : $(OBJS_DIR) $(OBJS_DIR)/$(TARGET)
$(OBJS_DIR) :
mkdir -p $(OBJS_DIR)
$(OBJS_DIR)/$(TARGET) : $(OBJS)
ar qc $# $^
$(OBJS_DIR)/%.o : %.cpp
$(CC) -c $(CFLAGS) -o $# $<
The response at the previous post (Previous Post) helped only if I moved alot of things around. I can't do this. So the question still remains, how do I get make to recognize the implicit build in a subdirectory from the main Makefile?
The error I am getting is
make: *** No rule to make target `release64/./Graphic/SymbolXLib/CartoCursor.o', needed by `release64/libSymbolXLib.a'. Stop.
I have to think you'd have far better success if you avoided include and instead use recursive make. In the top-level Makefile, something like:
graphic:
$(MAKE) -C Graphic
And the Makefile in Graphic/Makefile can have its sub-projects:
symbolxlib:
$(MAKE) -C SymbolXLib
and so on. You might need to add each of the targets to a default target or something similar to hang them all together on a single execution. You could give each of these targets an actual dependency (they should be .PHONY: if they don't have a dependency...) to rebuild them only when necessary or when commanded to by an upper-level target that touch(1)es "command files".
Alternatively, this paper recommends a different approach to avoid recursive make, but I've not yet read it -- and have found recursive make works well enough in projects I've been a part of that I don't mind recommending it.
Does this gnumake documentation help you?

Simple makefile generation utility?

Does anyone know of a tool that generates a makefile by scanning a directory for source files?
It may be naive:
no need to detect external dependencies
use default compiler/linker settings
You can write a Makefile that does this for you:
SOURCES=$(shell find . -name "*.cpp")
OBJECTS=$(SOURCES:%.cpp=%.o)
TARGET=foo
.PHONY: all
all: $(TARGET)
$(TARGET): $(OBJECTS)
$(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $#
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJECTS)
Just place this in root directory of your source hierarchy and run make (you'll need GNU Make for this to work).
(Note that I'm not fluent in Makefileish so maybe this can be done easier.)
CMake does it and it even creates makefiles and Visual Studio projects. http://www.cmake.org/
All you need to do is creating a CMakeLists.txt file containing the follwing lines:
file(GLOB sources *.h *.c *.cxx *.cpp *.hxx)
add_executable(Foo ${sources})
Then go into a clean directory and type:
cmake /path/to/project/
That will create makefiles on that clean build directory.
This is what I would use for a simple project:
CC = $(CXX)
CXXFLAGS += -ansi -pedantic -W -Wall -Werror
CPPFLAGS += -I<Dir Where Boost Lives>
SOURCES = $(wildcard *.cpp)
OBJECTS = $(patsubst %.cpp,%.o,$(SOURCES))
all: myApp
myApp: $(OBJECTS)
The only restriction is that if you are building an executable called myApp. Then one of the source files should be named myApp.cpp (which is where I put main).
There's a very old script called 'makedepend' that used to make very simple makefiles. I've since switched over to cmake for almost everything.
Here's the wiki article http://en.wikipedia.org/wiki/Makedepend, note the list of Alternatives at the bottom including depcomp in automake, and the -M flag in gcc.
EDIT: As someone pointed out to me in another question, gcc -MM *.cpp > Makefile produces a rather nice simple makefile. You only have to prepend your CPPFLAGS and a rule for constructing the entire binary... which will take the form:
CPPFLAGS=-Wall
LDFLAGS=-lm
all: binary_name
binary_name: foo.o bar.o baz.o biff.o
no need to detect external dependencies
use default compiler/linker settings
Why script then? Provided that all your project source files are *.cpp and in current directory:
all: $(notdir $(CURDIR))
$(notdir $(CURDIR)): $(subst .cpp,.o,$(wildcard *.cpp))
$(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $#
The Makefile would build the all the source files with default compiler/linker settings into an executable named after the name of the current directory.
Otherwise, I generally recommend people to try SCons instead of make where it is much simpler and intuitive. Added bonus that there is no need to code manually clean targets, source/header dependency checking is built-in, it is natively recursive and supports properly libraries.
As described in the linked discussion, HWUT is a tool that
can generate pretty Makefiles, searching for dependencies and include files in directories that you tell it. On windows you need to install MinGW and Ctags. Under Linux gcc and ctags are most likely present. It is OpenSource and free to use.
Especially, when generating Unit Tests for some already existing modules of some larger project with bad cohesion, this feautures easily spares you hours or even days.

Object files generation and best practices for linking using makefiles - C++

Background
I am just getting started with C++ programming on LINUX. In my last question, I asked about best practices of using makefiles for a big application. "SO" users suggested to read Miller's paper on recursive makefiles and avoid makefile recursion (I was using recursive makefiles).
I have followed miller and created a makefile like the below. Following is the project structure
root
...makefile
...main.cpp
...foo
......foo.cpp
......foo.h
......module.mk
My makefile looks like the below
#Main makefile which does the build
CFLAGS =
CC = g++
PROG = fooexe
#each module will append the source files to here
SRC :=
#including the description
include foo/module.mk
OBJ := $(patsubst %.cpp, %.o, $(filter %.cpp,$(SRC))) main.o
#linking the program
fooexe: $(OBJ)
$(CC) -o $(PROG) $(OBJ)
%.o:
$(CC) -c $(SRC)
main.o:
$(CC) -c main.cpp
depend:
makedepend -- $(CFLAGS) -- $(SRC)
.PHONY:clean
clean:
rm -f *.o
Here is the module.mk in foo directory.
SRC += foo/foo.cpp
When I run make -n, I get the following output.
g++ -c foo/foo.cpp
g++ -c main.cpp
g++ -o fooexe foo/foo.o main.o
Questions
Where should I create the object(.o) files? All object files in a single directory or each object files in it's own modules directory? I mean which is the best place to generate foo.o? Is it in foo directory or the root (My example generates in the root)?
In the provided example, g++ -c foo/foo.cpp command generates the .o file in the root directory. But when linking(g++ -o fooexe foo/foo.o main.o) it is looking for the foo/foo.o. How can I correct this?
Any help would be great
Where should I create the object(.o) files? All object files in a single directory or each object files in it's own modules directory? I mean which is the best place to generate foo.o? Is it in foo directory or the root (My example generates in the root)?
I find it easier for investigating failed builds to localize object files in a separate directory under the module level directory.
foo
|_ build
|_ src
Depending on the size of the project, these object files are grouped to form a component at a higher level and so on. All components go to a main build directory which is where the main application can be run from (has all dependent libraries etc).
In the provided example, g++ -c foo/foo.cpp command generates the .o file in the root directory. But when linking(g++ -o fooexe foo/foo.o main.o) it is looking for the foo/foo.o. How can I correct this?
Use:
g++ -o fooexe foo.o main.o
+1 for SCons.
I am using SCons, too. It scans the dependencies for you and it only rebuilds when source has changed as it uses cryptographic hash sums instead of timestamps.
In my SCons build the objects live in parallel directories to the source (to enable multiple builds like combinations of 32bit and 64bit, release and debug):
src
.build
linux
i686
debug
release
x86_64
debug
release
With regards to object and other generated interim files, I put these in a directory completely separate from the sources (I.e. under a directory that is excluded from backup and revision control). It may be slightly more bother to setup in projects or makefiles, but it saves time packaging up sources, and it is easier to have clean backups and revision control.
I create a subdirectory structure for the object files that matches the subdirectory structure for sources. Typically I have a separate subdirectory for each of my libraries and programs.
Additionally I also use multiple compilers (and versions) and multiple operating systems, so I will reproduce the object file directory structure under a directory for each of these compilers (which have newer versions of the standard and vendor libraries) to prevent object files with mismatched included header file versions.
The best thing you can do for yourself is to use something better than Make. SCons is my tool of choice on POSIX systems. Boost also has a build tool that is very flexible, but I had a hard time wrapping my head around it.
Oh, and if you want to use make, go ahead and build recursive makefiles. It really isn't that big a deal. I worked on a gigantic project using tons of recursive makefiles over the last three years, and it worked just fine.