
A Simple Makefile for Medium-Sized C/C++ Projects - ingve
https://spin.atomicobject.com/2016/08/26/makefile-c-projects/
======
onli
I do like simple makefiles. I adopted a C++-project without having much
C++-knowledge, and simplifying the build setup was a big step in making it
mine.

Thinking that it might be nice, I tried this one instead of my own approach at
a simple makefile
([https://github.com/onli/simdock/blob/master/Makefile](https://github.com/onli/simdock/blob/master/Makefile))
and I get this error:

> Makefile:16: __* missing separator. Stop.

Which raises kind of an important point: Simple means understanding what is
happening. I have no idea why `$(CC) $(OBJS) -o $@ $(LDFLAGS)` does raise that
error - and just like that it is not simple for me anymore.

 _Edit:_ Now with the tab-error out of the way, it of course does not find the
dependencies. That might be caused by a strange project organization (like
said, it is an adopted project) or maybe the makefile is just not complete –
I'm not sure how far the auto-generation is supposed to go. Played a bit with
it, but that just removes the simplicity. For now, I think that the pkg-config
way is nice and explicit, and would recommend to take a look at the linked
makefile if a simple one is searched. Though I still would be happy about
optimizations for that file!

I of course just might miss the point. Well, after all that supports mine: Not
that simple after all.

~~~
revelation
Your editor probably automatically converted tabs into spaces. And just like
that, the Makefile stops working.

Which proves the point: Makefiles are at a point in life where they should be
exclusively automatically generated.

The only exception are throwaway projects where you and only you use it.

~~~
mbrock
I strongly disagree. The tab requirement for Make is very well known and less
strange than custom Makefile generation. Simple Makefiles work extremely well
in lots of projects.

~~~
revelation
Somebody listed a number of common build system requirements below, but of
course there are usually many many more like custom compilers, cross-
compilation, any number of things that your "simple makefile" can't work with.

By the time the Makefile has grown to support all these things it no longer
has the properties that made it useful in the first place (parsimonious,
understandable, small).

~~~
anjbe
Supporting custom compilers and cross‐compilation is as simple as using ${CC}
in the Makefile instead of gcc…

~~~
revelation
This is how we got into this mess in the first place. "Surely it can't be very
difficult, let me just add this.."

~~~
anjbe
Perhaps, but I suspect writing a portable Makefile is not as difficult as you
think.

Speaking as a packager, I have to patch _many_ CMake build systems for
portability issues. And don’t even get me started on SCons. Autotools is
better in that regard, but has many flaws of its own.

Here’s an example of a portable Makefile I’ve contributed to a project which
handles the common packaging issues (staging directories, cross‐compilation,
parallel builds):
[https://github.com/sinamas/gambatte/pull/6/commits/8d6fffc2b...](https://github.com/sinamas/gambatte/pull/6/commits/8d6fffc2b3614f3753aae30bf78e66626a88d428)

Compared to the original build system (two SConstructs):
[https://github.com/sinamas/gambatte/blob/master/gambatte_sdl...](https://github.com/sinamas/gambatte/blob/master/gambatte_sdl/SConstruct)
[https://github.com/sinamas/gambatte/blob/master/libgambatte/...](https://github.com/sinamas/gambatte/blob/master/libgambatte/SConstruct)

------
Merad
It always annoys me that we don't have better solutions for building C and C++
projects by now. My own take on the generic drop-in makefile[1] has over 1000
stars on github, which says pretty loudly to me that this is a common pain
point for many people. Alas I haven't had a brilliant idea for a solution
(yet).

[1]:
[https://github.com/mbcrawfo/GenericMakefile](https://github.com/mbcrawfo/GenericMakefile)

~~~
realharo
True. Other languages have it way easier because:

\- proper module system, none of this header file nonsense

\- no compiler flags to worry about

\- no preprocessor definitions to worry about

\- as a consequence of the above, no dependencies that require their own
linker flags, include directories and preprocessor definitions to work

\- no need to support different compilers/platforms with different
configurations (which work differently with libraries that require different
linker flags and include directories and...)

~~~
72deluxe
Out of curiosity, how do other languages cope with:

a. Targeting different release types (debug vs release)

b. Changing behaviour of the compiler

c. Having different code paths between debug and release (#ifdef _DEBUG)

d. Having macros and helpful macro expansion

I do not understand the dislike of header files: how would you recommend
distributing DLLs and libs while exposing their functions to be developed
against?

I suppose you could implement everything in a header file if you wanted a
simple "module" system (thinking of the header as a module), but this would
establish a very simple system with no interdependencies or reusability.

~~~
winstonewert
> I do not understand the dislike of header files: how would you recommend
> distributing DLLs and libs while exposing their functions to be developed
> against?

Do you enjoy writing out all your function signatures twice? Haven't you ever
wished the compiler could automatically build the header file for you?

------
rwmj
Please don't brew your own build system. It likely won't handle at least one
of the following situations:

* installing into a DESTDIR

* setting custom CFLAGS/LDFLAGS

* changing prefix, libdir, sysconfdir

* cross-compilation

* parallel builds

* conditional features

* missing requirements

and will make packaging your software in Linux distros much harder than it
needs to be.

~~~
pja
There’s no point telling people not to do something if you don’t give them an
alternative that does do all those things.

(And no, “just use autotools” is not a viable answer.)

~~~
rwmj
Autotools _does_ do all of those things. I'm not a particular fan of it
either, but when I'm packaging software for a Linux distro then I'd rather see
autotools or cmake being used instead of some crappy custom build system that
will require patching to make it work.

There is room for someone to come up with a good build system. It had better
do everything autotools and cmake do, and be well documented and widely used,
so there's a rather large barrier to entry.

~~~
_RPM
Auto tools is great. To compile all you do is

    
    
        ./configure
          make
          make install
    

It couldn't be simpler. Even though I am part of the GitHub generation, I
don't see why so many of my peers hate it. I actually like it.

~~~
flohofwoe
This only works in the Unix world, on Mac only for command line tools (but
only after installing autoconf and make), and not at all on Windows unless you
install a complete Unix environment (cygwin or mingw).

~~~
slrz
Why would it only work with command line programs on the Mac? That's
definitely not true, unless something changed _very_ recently.

Also, you don't need autotools itself to build a autotoolized program. The
generated configure script is portable sh.

------
Davidbrcz
Just use cmake or scons. In 2016, you should not hand write a single makefile
!

A simple cmake/sconcs build file will be as simple as the one shown (or even
simpler !) and it will definitively will be easier to integrate external
dependencies if you have to.

~~~
elcct
Would be nice if you could give an example, not just "use that".

~~~
onli
Especially since scons and cmake are in no way easy to learn, imho.

~~~
realharo
Still hell of a lot simpler than hand-writing Makefiles though. For both small
and large projects.

I mean, a "hello world" CMakeLists.txt can be literally just this:

    
    
        add_executable(hello hello.c)
    

And it's cross-platform, can generate Visual Studio/Xcode/... projects, etc.

~~~
onli
I think we have to go deeper than that to really see an advantage of cmake. A
corresponding makefile would be:

    
    
        all: hello
    

That's not really complicated either.

~~~
radarsat1
Exactly. When evaluating a language / build system / whatever, it is
definitely not sufficient to look at Hello World. You have to evaluate the
worst case and average case if you want to get an idea of what you're in for.
Looking at the "best case" (simple one-file project) tells you almost nothing
except how hard it is to get started.

~~~
ryandrake
When evaluating build systems, one of my major questions is: How much "magic"
will I have to work-around in order to do something off the beaten path that
the build system's developers never considered?

This is a good criteria for languages as well.

------
oso2k
This is a good start. Could be made a bit more minimal.

I'll throw my base Makefile into the fray [0].

It supports cross compilation, CFLAGS & LDFLAGS usage, dependency file
generation, source file autodetection (add the file to right folder and it's
automatically included in the build), library building (`make lib`),
Continuous Integration (`make start_ci/stop_ci`), Continuous Testing (`make
start_ct/stop_ct`), Continuous Deployment (`make start_cd/stop_cd`),
Installation (`make install`), Uninstallation (`make uninstall`), parallel
compilation (`make -j4`), there's some build machine
detecting/autoconfiguration support, build artifact generation, and even some
git shortcuts (`make gstat/make make gpush`).

All in about 190 lines of Makefile (including some comments, and lots of blank
space lines). It does depend on GNU watch if you use any of the `start_ _/
stop__` targets. I'm about to add self-documentation to it as well.

For more minimal, comprehensible approaches, see the work of the suckless
people [1][2][3].

[0] [https://github.com/lpsantil/NewProj](https://github.com/lpsantil/NewProj)

[1]
[http://git.suckless.org/dwm/tree/Makefile](http://git.suckless.org/dwm/tree/Makefile)

[2]
[http://git.suckless.org/sbase/tree/Makefile](http://git.suckless.org/sbase/tree/Makefile)

[3]
[http://git.suckless.org/ii/tree/Makefile](http://git.suckless.org/ii/tree/Makefile)

~~~
posterboy
> a bit more minimal

ie. smaller, which is ironically smaller than the quote

edit: compared to 'gcc $CFLAGS $LDFLAGs <...> *.c' it's all rather bloated

------
kmm
Neat! I've been putting off learning how to use those `-M` flags to
automatically generate dependencies, but it looks like it's not that hard :)

What I also like to do is hide the actual compilation command with recipes
like this, inspired by the Linux kernel

    
    
        %.o : %.c
            @echo "      CC $<"
            @$(CC) -o $@ -c $(CFLAGS) $<
    

I think it looks a lot tidier, it turns a ton of dense uninteresting command
lines into a few dozen short lines.

~~~
andrewaylett
In a past life, the mega-but-autogenerated Makefile I helped maintain would
store the executed command and only echo it if the command failed. And we
logged everything to file, so you got nice output but all the info was there
if we needed it. Didn't take that long to work out how to do it, once we'd
decided it would be a nice thing to have.

~~~
qznc
I like that. It lives the Unix philosophy to be silent unless an error occurs.

------
bobbyi_settv
This approach is flawed because it uses "find" to discover the source files
which means that if you delete a source file, it won't cause a rebuild since
it won't see any changed files.

~~~
stormbrew
It's probably possible to adapt it to do that by using find much like you use
gcc -MM to get header deps, generating a make snippet that you then depend the
executable output on. But then we're getting pretty nasty.

------
flohofwoe
The problem with Makefiles is that they don't work on Windows with Visual
Studio, and generally only have very limited support in IDEs like Xcode,
QtCreator, etc... and once stuff like cross-compilation and feature selection
comes in, they get really messy really fast. That's where meta-build-systems
like cmake etc come in. They all have their faults (cmake has a terrible
scripting language), but they solve real problems for non-trivial projects.

~~~
makecheck
I agree that cross-platform is tricky and in the past I have used SCons with
some success for that.

Although, at least for Xcode, I can build my entire Xcode project from a
Makefile: you just have to use command-line tools like "xcodebuild", and if
you want a clickable version you set up a build phase that runs "make". And,
while Apple’s ".xcconfig" files are not exactly "make" syntax, they support a
subset that looks exactly the same so it is possible to have files with
variable settings, etc. that can be referenced by both Xcode and any Makefile
that needs to stay in sync. Environment variables are yet another option.

------
GreaterFool
I'm going to take this opportunity to rant about sad state of C++ open source.
I would _really_ _really_ like to write some C++ but I'm not a C++ guru; so I
tried installing facebook/folly and facebook/fatal on my Mac OS. That just
doesn't work. It wasn't written cross platform and it probably never will. So
I spun up a ubuntu VM and tried there. fatal compiles, folly still doesn't...
And so ends my brief foray into C++. At that point I started researching how
can I bind the library I need to Rust so I can skip all this nonsense.

C++ is not a bad language and I would like to write a project hat uses it. But
I'm just a user of the language. I don't have time and expertise to tackle
those build issues and quite frankly installing any library is a major PITA.

~~~
wyldfire
C++, regardless of open/closed source is not in a sad state IMO. The problems
you describe sound like they're from the perspective of someone who's worked
with much higher level languages. C++ is as good (or as bad) as it's always
been, it hasn't evolved to a bad state.

New language designers have the luxury of having been bit by C/C++'s
misfeatures and wrinkles. They also have the luxury of high end computers
which can solve lots of problems in reasonable computation time. This means
that modern languages can afford to trade off more computation resources in
favor of development simplicity.

> But I'm just a user of the language. I don't have time and expertise to
> tackle those build issues and quite frankly installing any library is a
> major PITA.

I have a hard time understanding what "just a user" of the language means. It
sounds like you're confessing to being a novice. That's fine, but you should
be prepared to have to do a lot of work before you become proficient in C/C++.
Much more so than modern languages.

------
gkya
BSD make (bmake) and its .mk includes are really simple, it's basically make
with batteries included. Many makefiles may be reduced to two lines while no
new syntax is introduced:

    
    
      PROG=${.CURDIR:T}
      .include <prog.mk>
    

It indirectly extends original BSD make. There are some syntactic differences
from GNU make, but no big depart.

See
[http://www.crufty.net/help/sjg/bmake.htm](http://www.crufty.net/help/sjg/bmake.htm)

Here is a custom BSDmakefile for my website, a more extensive example where
the mk libraries can't apply:

    
    
      # makefile -- Build the website.
      #
      # $Id: makefile,v 1.10 2016/08/15 12:55:49 igk Exp $
      #
      #
      
      M4=m4
      AWK=awk
      SED=sed
      TIDY=tidy5
      TARGS=-i -utf8
      
      include config.mk
      
      # Common templates for each output file.
      MACROES=html.m4 config.m4
      TMPL=${MACROES} meta.m4 header.html.m4 HERE footer.html.m4
      HTML+=${LISTS:S/$/.html/}
      ATOM=${LISTS:S/$/.atom.xml/}
      OUT=${HTML} ${HTML:S/html$/&.m4/} ${HTML:S/html$/&.dirty/} ${ATOM}
      
      .MAIN: ${HTML} ${ATOM}
      
      # Generate lists.
      .for L in ${LISTS}
      $L.txt: $L.list
      	${AWK} -f list.awk ${.ALLSRC} > ${.TARGET}
      
      $L.atom.xml: $L.list
      	${AWK} -f atom.awk ${.ALLSRC} \
      		| ${M4} ${MACROES} - \
      		| ${SED} '1,2d' > ${.TARGET}
      .endfor
      
      .ifndef NOTIDY
      
      .for P in ${HTML}
      $P : ${P}.dirty
      	${TIDY} ${TARGS} ${.ALLSRC} 2>/dev/null > ${.TARGET} || true
      .endfor
      
      .for P in ${HTML}
      $P.dirty : ${TMPL:S/HERE/$P.m4/}
      	$(M4) ${.ALLSRC} > ${.TARGET}
      .endfor
      
      .else
      
      .for P in ${HTML}
      $P : ${TMPL:S/HERE/$P.m4/}
      	$(M4) ${.ALLSRC} > ${.TARGET}
      .endfor
      
      .endif # NOTIDY
      
      .for P in ${HTML}
      ${P}.m4 : ${P:S/html$/txt/}
      	$(AWK) -f page.awk ${.ALLSRC} > ${.TARGET}
      .endfor
      
      clean:
      	rm -f ${OUT}
      
      .PHONY: uuid pub ssh anal
      uuid:
      	sh ./make-uuid.sh
      pub:
      	sh ./publish.sh
      ssh:
      	sh ./s.sh
      anal:
      	sh ./analytics.sh

------
greydius
We must be using a different definition of simple. I've never seen a simple
makefile.

~~~
wyldfire
Well, with implicit rules IMO you can get really simple but productive
makefiles.

e.g. (not "medium-sized" project, but illustrates the simplicity)
[https://gist.github.com/androm3da/9b5575fb288045775c178a4fb2...](https://gist.github.com/androm3da/9b5575fb288045775c178a4fb2483438)

here's a slightly more feature-rich example, still relatively simple IMO.

[https://gist.github.com/androm3da/9126ff51b5b47f69b99b1e3db8...](https://gist.github.com/androm3da/9126ff51b5b47f69b99b1e3db845d85b)

------
zbuf
Nice to see people spreading good practices with Make.

Too many folks don't understand good use of Make and thus are doomed to re-
invent it badly.

I've had to use too many build systems that are, basically, big and flat lists
of variables you can set. And yes I'm pointing at everything from esoteric in
house systems to mainstream ones. Make's system of overrides is something
especially useful.

~~~
raarts
This. Make is highly underrated. It's old so it's probably clunky, has a weird
syntax and dog slow. Well no. So many examples of people reinventing the
wheel, it's just sad.

------
TickleSteve
"But because Make works backward from the object files to the source, we need
to compute all the object files we want from our source files".

No.... don't do this.

Make is intended to work the other way around... it finds the source from your
targets.

Define your VPATH and let it find them.

This is what loads of people do and is why there are _so_ many Makefiles are
packed full of macro-magic!

~~~
Tyr42
I do agree that VPATH is helpful, but you'd still need to put the Makefile
into the build directory in that case, which is distasteful, as it's not a
built file.

Also you can't really get around specifying the files or searching, unless you
have one c file per executable.

------
lttlrck
I feel like the only person in the world that uses Boost Bjam (for a
proprietary project). I feel like I should switch but every time I look into
it and read comments it's not at all clear that Bjam was a bad decision - even
if it feels like I was the only one to choose it :-) It's simple, seems pretty
fast but it is very hard to integrate custom build steps.

------
imron
I'm a big fan of meson [0].

The one thing it doesn't have over this Makefile is automatic detection of
source files - you need to manually specify them. I haven't found that to be
prohibitive in practice.

0: [http://mesonbuild.com/](http://mesonbuild.com/)

------
ape4
Jam is nice and simple
[https://www.perforce.com/resources/documentation/jam](https://www.perforce.com/resources/documentation/jam)

------
danidiaz
Translating that Makefile to Shake
[http://shakebuild.com/](http://shakebuild.com/) would be a good Shake
learning project...

------
vanilla
This is not simple. I haven't made my own Makefiles, because when I see them,
they are complex and not easy to understand.

That's why I chose CMake for my projects. If this is simple, I chose the right
build tool.

This build file of a large project is easier to understand than the "Simple
Makefile"

[https://gitlab.kitware.com/vtk/vtk/blob/master/CMakeLists.tx...](https://gitlab.kitware.com/vtk/vtk/blob/master/CMakeLists.txt)

~~~
raarts
> they are complex and not easy to understand.

Makefiles are declarative, which is a bit strange at first. If you start with
a simple one you pick it up really fast.

------
banachtarski
Can we kill raw makefiles already? Projects that don't handle out of source
builds in a sane way are awful to integrate.

