A Simple Makefile for Medium-Sized C/C++ Projects
spin.atomicobject.com
spin.atomicobject.com
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) 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.
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.
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).
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...
Compared to the original build system (two SConstructs): https://github.com/sinamas/gambatte/blob/master/gambatte_sdl... https://github.com/sinamas/gambatte/blob/master/libgambatte/...
I actually did know that once, but forgot. Make definitely has its hidden traps.
The commands for implementing rules are always indented by a tab. This is a classic makefile newbie make gotcha & everyone gets bitten by it at some point or other.
I haven't tried CLion yet but I would assume it's decent given JetBrain's track record.
- 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...)
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.
a. Different build goals with different settings in a build file.
b. Build steps can be changed in the build file but there are a lot of sane defaults.
c. Plain constants and if statements. The compiler optimizes away dead code.
d. Code gen is done as another step before the build and not really a task of the compiler.
a. debug/release doesn't matter much so all Java code is in "debug" mode (http://stackoverflow.com/questions/8613535/does-java-have-de... ); the actual debugging is handled via source code/source code jars (jars are just zip files with a standard structure) -> Maven: https://maven.apache.org/plugins/maven-source-plugin/jar-moj...
b. configuration files -> Maven: https://maven.apache.org/plugins/maven-compiler-plugin/compi...
c. don't do that? :)
d. no macros and most people seem to agree that's a good thing; C macros are not really the kind of tool you want to be using, unless forced to; code generation is used instead
Regarding header files: Javadoc for humans, code inspection on the class files for IDEs. No need for separate files in a system designed after the 70s :)
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?
If you're still writing small projects and insisting on running a build in a command window, or writing code in a simple editor then I can perhaps understand the complaint. But you quickly grow out of that when you start writing larger projects.
I couldn't imagine the 2+ million line codebase at work using hand-written makefiles. It builds for debug, release, MBCS and Unicode, and with different versions from source control every night, so multiple versions for different branches are built.
At least there's autotools, which has its own set of problems, but you can be reasonably confident it'll generate a portable Makefile.
Are there any other make systems that have Project Mode like qmake does?
* 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.
(And no, “just use autotools” is not a viable answer.)
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.
./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.It’s a crawling horror for the programmer that wants to use autotools as the build solution for their code however, which is the topic under discussion.
Also, you don't need autotools itself to build a autotoolized program. The generated configure script is portable sh.
It does all those things. It's fast. It's cross-platform. The language is Python.
It's a bit steep to learn and maybe doesn't have the most mature built-in recipes for certain complex builds like mobile or desktop frameworks, but it seems like those things would be addressed quickly if more people were using it. The maintainer is a class act, consistently putting out new features and bug fixes and working with pull requests.
swtoolkit made scons really nice to work with (but couldn't fix the speed issues). It's a shame it was abandoned. In the same way, a thin layer of functions as demonstrated in the waf "toolkit" examples could lean on the power of waf and provide a build system every bit as snazzy as big, polished systems like bazel and buck, but it would work cross-platform out of the box and be extensible with only Python. But I think waf is close enough to being high level that no one bothers to create and polish a build system layer (maybe internally in companies, but the open source uses I've seen use pretty standard waf scripts).
After packaging hundreds of programs for a GNU/Linux distro and dealing with handwritten Makefiles and other so-called autotools replacements: Yes, it is.
If you want someone to be able to build and install your software, just use the Autotools. Yup, they are ugly, but they're still the only set of tools that works acceptably.
It's a pleasure when autotools is used because in generally I know it's going to be nothing much more than
> --with-stuff/--without-stuff
and
> --prefix=$HOME/.local
and when it's not, at least I know I can just grep the shell script and figure it out pretty quickly without having to learn a whole new programming language.
> setting custom CFLAGS/LDFLAGS
> changing prefix, libdir, sysconfdir
> cross-compilation
Pretty much all of this can be done trivially in a makefile, using a very innovative construct called "variables".
> parallel builds
make -j. Works especially well if you have working dependencies. This makefile does.
> conditional features
make has ifdef... Granted this makefile uses find to discover source files, which won't totally work. But it's a small fix. Or you can do conditional features through CFLAGS.
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.
make hello
Easier?
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. all: hello
That's not really complicated either.This is a good criteria for languages as well.
Program("main", ["main.cpp"])
The problems start once you want configure stages, build directories, portability and install targets, as all of those aspects are extremely lackluster and incomplete in `scons` and you have to reinvent large parts of the build system yourself essentially for anything even mildly complex.`cmake` is much better. The language itself is ugly and takes some getting used to, so it's a little more complicated to get started with then `scons`. `cmake` generating `Makefile`s instead of actually building the project itself also adds a layer of complexity. But once you understand the basics it's much easier to get a fully functioning build going, including configure stages, build directories, portability and all that, as cmake has all of that integrated and working right out of the box.
`cmake` feels like a complete build system, while `scons` feels like a good start for a build system that was abandoned at the halfway point.
I still use plain `make` for some Python projects (mostly .PHONY targets to run `pylint`, `autopep` and friends), but for everything that needs to get compiled `cmake` is much easier to use than plain `make`, as you don't need to reinvent a build system yourself, it already comes with almost everything you need.
demo.cxx demo_b.cxx CMakeLists.txt
$ cat src/CMakeLists.txt
cmake_minimum_required (VERSION 2.8.11)
project (HELLO)
add_executable (helloDemo demo.cxx demo_b.cxx)
$ mkdir bin
$ cd bin
$ cmake ../src
SNIP
$ make
$ ./helloDemo
$ scons
CMake is almost great, but I have no idea why they went ahead and designed the _language_ the way they did. (Lists are concatenated strings, everything is basically macro expanded, etc..) I mean, if they were trying to improve on m4 and shell script, I wouldn't exactly say they succeeded.
I realize that CMake has better cross-platform support, but I can't stand using it. For small projects it's fine, but for medium- to large-sized projects it's a bigger headache than autotools imho.
(That said, I explicitly don't care about Windows support, which is the only place where it really matters to have something different than autotools, and even that is not so bad with MingW and will be even better, it seems, with the coming "linux-compatible subsystem")
1. Linux target: "Wow this is a breeze!"
2. Mac target: "Ahh, a little learning curve, but no problem!"
3. Windows/VC target: "Jeez, I hope I don't have to hand-edit this monstrous vcproj this thing generates..."
4. iOS: "WTF!! Simulator targets, device targets, provisioning profiles, code signing.. This stuff was all just hacked into cmake and barely made to work wasn't it??"
5. Android+NDK: "Kill me now"
6. Other obscure mobile targets with toolchains themselves that barely worked: "La La La, I'm ready for the funny farm. The cucumbers! They're dancing! And glowing!! The room is getting hazy and dark.. Where's my vodka?"
It sounds fun to read.
Most of the difficulty to be honest was with: 1. third party libraries, and 2 the Android NDK.
For third party libraries, every one was slightly different in some frustrating way. Some of them used bog-standard makefiles (thank you, third party developers who do this!), so they basically just worked. Some of them used autoconf/automake, which makes me sad but you can generally get cmake to mate successfully with them. Some of them helpfully had their own CMakeLists.txt, which would build the library, but invariably could never be successfully integrated into a larger project. Some non-open-source libraries came with just one platform's build support, so you'd have to hand-build CMake support for them, and guess which files actually needed to be built/linked. Some of them cleverly generated some of their own source files during the build process ("This is fun to maintain" said nobody ever).
The Android NDK seems to be some Googler's frankenstein hobby project that got accidentally electrified into life and set loose on the world. Like Donald Trump, the NDK is kind of an embarrassing offshoot from the "mainstream Android development story," one that Google would like to pretend doesn't exists. They, in fact, tell you in their docs "Yea, you can use native C and C++ to write Android apps with this thing, but you really shouldn't!" [1] Seriously, their "Getting Started" guide spends a paragraph or so trying to convince you not to get started. Convincing Google's own development tools to build C++/NDK is pretty frustrating itself, so you can imagine how difficult it is to convince cmake to do it.
In short, if you are working on a project that targets desktop, embedded, and mobile platforms and has lots of third party code, and one day that little voice in your head starts saying "Hey we waste a lot of time maintaining separate Makefile, .xcodeproj, .vcproj, etc. Why don't we switch to a single glorious build system that lets us list our source files in one place!?" IGNORE THAT VOICE. Go play some foosball, fix a few bugs, have a cup of coffee, but by no means try to use cmake for this.
Thanks for the write up.
SCons has many issues that negatively affect packagers. Some are listed on the Gentoo wiki here: https://wiki.gentoo.org/wiki/SCons
As an OpenBSD packager, I hit many of those issues and a few more. For example, scons’s C preprocessor support is not great, and often brings in header dependencies that should be #ifdef'd out; when those unnecessary headers are removed during build (happens often on a machine bulk building packages), the build breaks.
I’ve replaced SConstructs with Makefiles before. Typically it makes the build faster, simpler, and more portable, and is popular with other OS packagers. Example: https://github.com/sinamas/gambatte/pull/6
It's not perfect, as others have touched on. But it overall satisfies my requirements and I can usually get things done with it.
Being a Google employee, I'm somewhat partial to Bazel (https://bazel.io/) which is based on what we use internally. I've started to ponder what it would take to write a CMakeLists.txt generator for it.
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
[1] http://git.suckless.org/dwm/tree/Makefile
ie. smaller, which is ironically smaller than the quote
edit: compared to 'gcc $CFLAGS $LDFLAGs <...> *.c' it's all rather bloated
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. Q ?= @
%.o : %.c
@echo 'CC $@'
$(Q)$(CC) -o $@ -c $(CFLAGS) $<
Now you can make the commands visible again via `make Q=`.A more subjective question is, if you echo $@ or $<.
Also you have to wait for find to run before any building starts. Can be slow for a large project.
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.
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.
I write all my OSX projects in C++ using it.
Learning to build projects and solve compiler and linker errors is part of the journey. Stroustrup's book takes longer to read than 5 minutes, so writing C++ will take longer. It is a rewarding journey, where you will suddenly find all other languages (Java, C#, C, JavaScript, Swift) very similar or simplified in comparison.
To get you started you could install xcode and the commandline tools, write your file.cpp and use:
g++ -o myprogram file.cpp
This assumes no linker options, no preprocessor options or anything, and on OSX will use clang, which it secretly symbolic links as g++. You might find it easier to get your head around it by starting a project in Xcode, setting the options in the IDE and then observing the output that it runs.
Note that installing a library on most Unix platforms isn't complicated - configure, make, make install.
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.
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
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.she.g. (not "medium-sized" project, but illustrates the simplicity) https://gist.github.com/androm3da/9b5575fb288045775c178a4fb2...
here's a slightly more feature-rich example, still relatively simple IMO.
https://gist.github.com/androm3da/9126ff51b5b47f69b99b1e3db8...
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.
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!
Also you can't really get around specifying the files or searching, unless you have one c file per executable.
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.
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...
Makefiles are declarative, which is a bit strange at first. If you start with a simple one you pick it up really fast.