A simple makefile is a unicorn
nibblestew.blogspot.com
nibblestew.blogspot.com
Here's a couple examples:
1. The "%.o: %.c" line is a huge code smell for Makefiles. You shouldn't ever write that line—it's built in!
2. Mentioning .h files in the Makefile. Don't. Go look up the `-MMD` flag in your C compiler. Clang, gcc and many other C compilers support it. Add that and a "-include *.d" line in the Makefile and you'll have great dependency support.
Everything else in that makefile is just setting variables. It's made overly complicated by the directory junk, but I'm failing to see the problem.
Doesn't that sacrifice portability, though? Is this method portable between GNU make and FreeBSD make and OpenBSD make?
The special handling is in the compilers: they emit make file parts!
[1] cf. http://pubs.opengroup.org/onlinepubs/9699919799/utilities/ma...
CC = clang
CFLAGS += -Wall -Wextra
libs = -lm
objs = hellomake.o
hellomake: $(objs)
$(CC) $(CFLAGS) -o $@ $(objs) $(libs)
.PHONY: clean
clean:
rm -f hellomake *.o *.d
deps := $(objs:.o=.d)
%.d: %.c
$(CC) $(CFLAGS) -MM -MF $@ $<
-include $(deps)
The first two lines tell us which compiler we're using and set up some extra compiler warnings. Then we specify which libraries we use (it's a matter of taste whether this is a variable or just spelled out later) and the names of our object files.The first target, hellomake, is our executable. This is the default target, so it's the one that'll be built if we run make with no arguments. It runs the C compiler with the flags we specified earlier. Its dependencies are the object files, which means they'll be built automatically and rebuilt when their dependencies change. As noted, there is a built-in rule to build the object files from the C source using our CC and CFLAGS. We don't need to tell make how to do that.
The next target, clean, is a "phony" target—it doesn't refer to a file. Normally a make target tells make how to build a file. A phony target, in contrast, is really just a shell script that doesn't produce any file output. This one deletes all the build products.
The remaining lines set up the dependency tracking for headers. This makes it so that a change in a header file will trigger a rebuild of any source that includes it. We begin by making a list of files by copying the list of object files and swapping in ".d" for the ".o" file extension. Next is a rule that tells how to build a .d file from a .c file. This rule runs the C compiler and tells it to output a dependency tracking file. The last line says to include each of the .d files in the Makefile. (The .d files contain Makefile syntax to specify the header dependencies.)
I omitted the IDIR and ODIR and such because in a project of this size I wouldn't bother putting includes and object files in separate directories from the source.
I hope that helps. I know make can seem outdated and weird but I think it really can be simple once you learn a few things about how it works. Hopefully this will get you started in that direction!
Please honour LDFLAGS and LDLIBS here.
> $(CC) $(CFLAGS) -MM -MF $@ $<
Please honour CPPFLAGS here.
Don't use wildcards in clean target. Use $(RM) $(OBJS) $(DEPS) instead.
But the question is whether this is Make's fault for being incomprehensible, or the User's fault for being lazy.
The thing is virtually any system or tool can be understood with enough practice, but just because there is someone out there who can use it well does not mean it's not poorly designed.
Both C and Make are decades-old technologies, but most people can read a C file without much experience and intuitively understand what is going on. The same is not true for Makefiles.
'make' exists for what it's capable of doing and it must be so as it's used in every piece of software and operating sytem everywhere.
So it's extremely popular/widespread, therefore it must be good, and therefore you should use it (thereby making it even more popular/widespread)?
I really hope that you poorly worded that/I'm grossly misunderstanding you because otherwise, your logic is batshit insane.
My experience has been that that people that truly understand and appreciate technology for its own sake are also likely to underestimate the value of a tool being popular and widespread. What 'popular' really means is that the tool is more likely to be where you need it to be and the people you need to use it will already know how. Those things outweigh a 'perfectly' designed tool that you don't have or know how to use.
There's a balance point, for sure. For instance, the pace of "innovation" in front-end javascript libraries throws out a lot of babies with the bathwater, and we might be well-served by accepting a paradigm for 10 minutes and building on it rather than always chasing after the next hot thing.
But at the other extreme, there was a time when PHP was ubiquitous for back-end development, and it stuck around largely because of inertia: the obvious flaws were accepted just because it was so accepted as the tool for that job. But I don't think many people are lamenting the fact that we've moved on to better tools for back-end development. In that case inertia was a bad thing because it delayed innovation.
For me an example of a "good enough" tool is GIT. It's not perfect, it's got a non-trivial learning curve, and surely some innovations could be made to improve version control. Some would already argue that Mercurial is the Betamax to GIT's VHS - but it does the job well enough, and everybody uses it, so it's good enough.
I'd argue Make is more like PHP than it is GIT.
Exactly this.
> I'd argue Make is more like PHP than it is GIT.
What would you want a make replacement to look like?
Make is pretty 'roll your own' in places, but I've found it to have a nice balance between expressiveness and terseness. For simple projects, there isn't much to do in make, and for the more complex projects I've been involved in (embedded work, a self-hosting compiler, etc.) make can still generally express the build process within the confines of Makefile syntax. (In contrast to more modern tools that might require plugins in a different language, etc.)
For the compiler project I mention, the build process includes building several executables in C, running them against a pre-built VM image to compile another VM image, translating that image into C code that is then compiled and linked into the VM to produce another executable, after which there's a unit test suite. Root rebuild command is 'make clean && make tested', and there are only a couple hundred lines of Makefile code to get there. (There's also cross-compile support, etc.)
I'll admit that there was a certain amount of effort involved in writing those Makefiles, but I've also implemented the same build process using a modern GUI tool and found it to be much more difficult.
Admittedly I haven't given a ton of thought to this specific problem, but here goes:
I actually like Make from a conceptual standpoint. I think it's a good idea to codify build orchestration in code rather than non-human-readable output from a GUI application. My main gripe is that it seems to be very much a 1.0 tool: it was build to solve a specific problem, and then features were added as needed. I would love to have an iteration on that concept which benefits from what we've learned from Make while rounding off the rough edges of what feels like the result of a largely ad-hoc design process.
As far as what that would look like: I would like to see a concise, low-level API for doing the kinds of things Makefiles do (i.e. defining a dependency graph, setting up build parameters etc), and better tools for composition.
Also I would sacrifice a bit of terseness for the benefit of readability. For instance an arbitrary keyword like `Phony` could be replaced by a more extensible name-spacing syntax which lets you know exactly what's going on.
While they might think they know what's going on, I'm not convinced they actually do. C can be an incredibly subtle language to get right...
http://blog.llvm.org/2011/05/what-every-c-programmer-should-...
Regarding make, the thing to realize there is that it's just a DSL for specifying a dependency graph (and the way for satisfying dependencies). It's not a hard concept, but unless you come to the field with a bit of computer science background, it's not likely something you've been directly exposed to at the level required to understand make. But I don't know that this means make's choice of abstraction is necessarily a bad thing for its intended use cases. I also think this tends to put the lie to the notion that the difficulty is mainly around tabs... tabs may obscure the details, but switching to a clearer syntax won't solve the more fundamental gaps in understanding the mechanics of make.
I'll also argue that this is a potential problem with DSL's in general... because DSL's often don't use traditional imperative language semantics, there's always a bit of a learning curve that needs to be ascended. However, for make at least, there are a useful debugging tools to help understand what it's actually doing.
For me, at least, 'make -n' is great in that it instructs make to resolve the dependency graph based on the current state, and then just print the commands it'll run without executing them. If you know what the commands do, and know what needs to run, this can be a huge help understanding what's going on. (And there are other more powerful debugging capabilities as well for dumping variables, etc.)
As with many 'simplified' DSLs that are often unsuited to the task at hand (e.g. C++ templating), make (or versions of it, anyway), evolved to accommodate these use cases and then morphed into something that accidentally became turing complete: https://stackoverflow.com/questions/3480950/are-makefiles-tu...
Isn't a C++ template the exact opposite of 'Domain Specific'?
Make is not a needlessly complex tool (unlike most garbage tools these days); it’s complex because it needs to be flexible enough to build any programmer’s software. Since ways to program and build software are infinite, the tool itself cannot be simple, otherwise trade-offs have to be made.
This is the very same narrative that vim users proclaim: "It's totally not complex if you took some years to learn your tools" — while simply not realizing that the whole Make syntax is a pile of clunks and hacks and stopgaps and bandaids that has a super steep learning curve and looks like incomprehensive gibberish to the uninitated eye.
And most of that needlessly so IMHO. It shouldn't be hard to find a better, well designed syntax with sane defaults, stricter rules with typing and linting, less surprises (tabs, I'm looking at you) that's overall much less scary for newcomers — as well as less taxing for "expert casual" users like me. (I still go for my coworkers and Stack Overflow regularly if I have to fix one and I'm a fucking CTO with 10 years of dev experience)
Make's only reason for survival is its ubiquity. No matter which terminal you boot up, it's just there, that's why we use it too, and I hate it every day.
I think none of the alternatives (cmake, Jam, Gradle, Maven, grunt etc. etc.) has really succeeded as a cross-platform, cross-ecosystem solution that's both an approachable, simple and usable version of a DAG execution engine that's usable for lots of situation.
So the situation will probably stay like with vim and the QWERTY keyboard, the status quo pretty bad, but everyone's too used to it to go for something better.
Sounds like a solid reason why makefiles are great to me then.
Vim is so good it finally is letting emacs-OS have a real editor.
Something interesting I see more often than not when someone I know decides to try out vim:
They know I'm a vim user so they ask me for good tutorials and I tell them there are lots out there but they should start by running vimtutor (included with vim). Takes around 15-20 minutes to work through the interactive lessons and gives you all the basics of movement and editing.
A day or so later I ask how it's going and the answer is usually that they're struggling with vim, so I ask if they ran vimtutor. Usually nope, or they started it but quit after a minute or two.
Why would one switch to a tool with a different paradigm, ask for and get a tutorial recommendation, and then ignore the advice of spending less than 1/2 hour on a tutorial that ships with the tool? sigh
In Vim there are modes that describe sets of keybindings and there are commands that are bound to them. The commands themselves are are straightforward for anyone who's fluent in Linux vocabulary.
'Saving' is a Windows word. We :write to files or :w for short.
I think that I am rather good at memorizing lots of obscure raw data - but in my experience it takes a lot longer to remember all the "obscure" commands of vim. In this sense I would not say it takes 15-20 minutes, but at least a day if at the end you really want to have the commands in your brain.
In this sense the mistake in your argumentation is the timescale that you are talking about.
Also, I hold some fairly controversial opinions: you don't need dozens of vim plugins; you don't need to compete with vim golf champions; you don't need an extensive configuration.
What you do need is to fully grok the idea of normal mode as an editing language with verbs, objects, and modifiers. Once you "get it" then you have a working knowledge of how to construct "sentences", and learning a new text object allows you to apply pretty much all verbs to it. Same for learning a new verb; you can apply it to lots of things.
Over the years I've learned a lot of interesting vim things and I've forgotten most of them. Because I don't need them every day or even every week. That's ok. The things I remember are things I use often, because they're useful in everyday editing.
Does it hurt to write it though? It makes the makefile more readable for people who don't know it's built-in. Besides, it's only built-in for ".c". I prefer to keep it in order to have a generic Makefile I can adapt to other programming languages.
Make has lots of built-in rules for lots of languages, C, C++, Fortran, Lex, Yacc, etc.
* Recursive Make Considered Harmful
* Changes to the Makefile do not implicitly trigger rebuilds
* Dependencies on directories is not really possible
* Lots of output by default
* Rules with multiple output files are not really possible
* can't handle spaces in pathnames
[0] https://github.com/qznc/annoying-build-systems#make-alone
* Change tracking entirely depends on modified timestamps instead of something more robust like checksums.
* Makefiles are always platform specific. Make executes commands based on a dependency graph, but it has no commands of its own. You either write multiple makefiles or you build everything in a bash environment.
I think in the context of Make it is ok. If you rely on checksums, you must implicitly assume that all commands you execute are idempotent. That assumption easily breaks.
Ceterum censeo, Clearcase must be destroyed.
This indicates that make is hard to understand. I've been using it for a while and I still struggle to grok concepts like order-only prerequisites.
I am completely satisfied with Make as a replacement. Took me a couple days of hacking to figure out how to set it up and it's been problem-free ever since.
By far, my favourite aspect is that I no longer need to depend on flimsy gulp-specific wrappers. I just call my build tools natively.
JS = $(shell find . -name "*.js" -o -name "*.json")
CSS = $(shell find . -name "*.scss" -o -name "*.css")
I use a small program called watch https://github.com/tj/watch written by TJ Holowaychuck (the inventor of the Express framework) which automatically re-runs my makefile every second. As make is smart enough look for modified files and rebuild only what is necessary, it works quite well. JS_FILES = $(shell find ./src -name "*.js")
JS_ENTRY = ./src/a.js
JS_OUT = ./build/scripts.js
$(JS_OUT): $(JS_FILES)
browserify $(JS_ENTRY) > $(JS_OUT)
Now anytime any js files within the src/ directory are modified, make will rebuild scripts.jsCheck it out: https://gist.github.com/molszanski/56de03b54f66885ef415af84e...
npm scrips are also awesome. But editing and maintaining them inside a package.json is not cool
Even for the example he uses, since he seems to be on GNU Make, it's a pretty perfect case to use Guile integration to handle that metaprogramming he's trying to do by hand.
I certainly think whatever incarnation of make that you would summon to do some sort of multi-machine configuration management (per your chef/ansible comparison) is something I would do everything in my power to avoid.
Many a time I see unslim and unclean Makefiles where the problem is mostly that someone started to use make then tried hard to use make as a single unifying tool for too many things, some clearly not belonging to make.
There is nothing wrong with writing wrapper and helper scripts or programs alongside a Makefile, so that make is only bothered with its dependency graph building.
History has shown that having sensible defaults for common use cases and workflows goes a long way to reducing complexity and ammunition for self-afflicted foot shooting should be isolated and explicit.
So if you invoke a tool that expects some arguments wrapped in parentheses, you need to do stuff like
LEFTPAREN:=(
RIGHTPAREN:=)
$(addprefix $(LEFTPAREN), $(addsuffix $(RIGHTPAREN), $(PARAMS)))
(RIGHTPAREN isn't strictly needed, but I introduced it to have the symmetry with LEFTPAREN, which is absolutely necessary)...like $(patsubst)?
$(patsubst %,(%),$(PARAMS))
Though the part about quoting still stands. And I couldn't be bothered with
checking whether the $(patsubst) is a function defined in SUS or is GNU specific. : fred@dejah ~ $; cat >hello.c
#include <stdio.h>
int main() {printf("hello\n"); return 0;}
^D
: fred@dejah ~ $; make hello
gcc hello.c -o hello
: fred@dejah ~ $;
How about that? make with no Makefile. The unicorn doesn't even exist. Which is as clean as it can get. make only resolves dependencies. Which is as slim as it can get. Early we agree on.You're free to love make all you want, but I've been there, was effectively held hostage by it for lack of better options, and hate it. Automake and asociated tools was the first attempt at getting us out of that hell, and it didn't succeed but cmake/premake/bazel are doing a whole lot better.
I think the problem here is the complexity of C, C++ and the building of such, not the makefile itself. Make can make simple things simple, but it can't take away genuine, real complexity as if it never existed.
I have lots of makefiles I use and am super-happy with. But none of them are for C-based projects. When you take away the complexity that C/C++ brings, you no longer need to bring that along to your makefile.
And you know what you have then? Apparently what the author refers to as a "unicorn": A nice and super-simple makefile. The unicorn is suddenly real.
However, a lot of other commenters in this thread have mentioned using make to manage their JS builds. JS build tools tend to work on whole directories of files together, and as such its fairly easy to compose these tools without a lot of logic in the build tool, and here make excells because its ubiquitous and simple. We use a makefile for building crystal and its simple and works great.
Make isnt useless, its just useless in the world it was created in.
Because many old programmers don't want to change their habit.
> including every piece of software installed on it?
Look up ninja.
We have tools like Maven and Ant and Scons and CMake because someone didn’t get Make and shell but they thought they did. Now I’m stuck with toy build tools which use unnecessary dependencies (like Java and .NET and Python) and only do as much as their programmer could imagine, limited by that programmer’s imagination (and experience). You bet your patootie I’m going to say NO! I don’t want to be stuck with a toy, someone’s misunderstanding of Make.
I've used make for a long time. I've never considered it difficult to learn or use. It has a limited number of things that it does for you, and leaves the rest to you. I like that paradigm a lot more than systems that seem to operate magically.
echo 'int main(){}' > a.c && make a
The only winning move is not to play. How about a nice game of chess?* Using declarative code as much as possible but still written in a good general purpose turing complete language (maybe python - not make).
* Built upon a foundation of well designed libraries, installable from a package manager that abstract common build workflows.
Make meets neither of these standards.
Of course, if you only care about POSIX, a lot of the make issues go away. Not all, but many.
I think it's possible to do better.
https://github.com/evoldoers/biomake
It uses the declarative programming language Prolog to write flexible rules.
A logic programming language like Prolog is very well suited for expressing rule-based dependencies.
In a simple C project packaged for Linux, FreeBSD anc Mac OS I seriously think a simple Makefile is possible.
And will continue to think that until I see an alternative that is easy to get going with on those systems.
For other use cases, there is no generic way to automate this process.
Make has its use. It's a great tool for building small/medium projects that don't have many dependencies. You can setup a new project in minutes with nothing more than a text editor and compiler. If you know *sh scripting, it's not really complicated. For bigger projects I prefer Bitbake to avoid "dependency hell".
You don't get to claim simplicity if you use rebar.
Yea, thats true. But good to see that I am not the only one who creates simplified makefiles to utilize other build tools.
My critique on erlang.mk is, that its far simpler to remember the 10-20ish commands of the rebar toolchain than thousands (or hundreds, but that it what it feels like) of erlang.mk options for every erlang tool that has ever been invented.
There is no significant difference between the two.
Makefiles are simple in that you can see every directive and very little is hidden from you. There is no magic - just dependency ordering using mtime as a very simple heuristic.
[1]: https://www.gnu.org/software/make/manual/html_node/Catalogue...
I also usually put `.SUFFIXES:`, but I guess that's redundant: "The `-r` or `--no-builtin-rules` flag causes the default list of suffixes to be empty." [1]
[1]: https://www.gnu.org/software/make/manual/make.html#Suffix-Ru...
In the purest Unix tradition, Make was designed to be tool-agnostic: you can choose which editor to code in, which (if any) preprocessor to use, which compiler and which linker to invoke and with which arguments, and what else to do aside.
And, all of this is controlled by a bunch of text files, which means grep and sed are your friends when you want to change a linker option for several projects.
Make comes from a philosophy which is the opposite of the IDE's ground. Not that the IDE concept is wrong per se, but if you are only used to have the same monolithic behemoth from code typing to binary output, your critics of make sound the same as an industrial fast food cook complaining that artisan patisserie is too complex.
make <executable name>
The next step up would be a Makefile that is essentially just a set of sh scripts, like your example. But I think it's supposed to be implied that the blog is specifically talking about medium-sized C projects.I know C but am not experienced working on large C codebases. Below I will describe its rough build structure. I hope for strong tool or re-framing advice.
I have an existing python codebase. I want to make it easy to create native-code plugins. The method I have worked out: I compile shared-libraries. Python then calls into these via python-ctypes.
Each plugin is a directory project/ext/plugin,
* Python code: __init__.py and ext.py
* C code, api.c
* A rough build script, build.py. This accesses global config (e.g. path information) held outside this directory. It compiles api.c into a .so or .dll. It does this by generating a shell script or batch file, and then shelling out to it.
A python script in the root directory walks through this structure and forks a process for each plugin build.py. It is multiplatform (currently linux gcc and windows mingw32). So I have if statements saying "if windows, generate this big string, otherwise generate that big string."
The shared-object is compiled to a separate directory, res/ext/plugin. I did this to create separation between my source and build artifacts separate. (Is this sensible?)
This naive build structure was easy to create, and allowed me to spend my focus elsewhere while I was bootstrapping. Now that is done, I am interested in how I could improve the build so that it could scale to a large number of plugins.
Would I be well-served to use Make or Cmake or something else? Can they access configuration from outside of their own directory? Maybe I should communicate configuration via environment variables?
Is your existing parallel builder too slow? What's the problem with it? Make can do parallel builds as well, but it won't walk a directory so you'd have to write a million targets.
I think you should post this on the SO site for programming design/advice though.
I think waf, which I've plugged before, would be a good choice for your project. Without knowing the details, your build script would look something like:
def configure(ctx):
# Load C compiler support
ctx.load('compiler_c compiler_cxx')
if ctx.env.DEST_OS == 'win32':
# Special thing for Windows
def build(ctx):
for plugin in ['a', 'b', ...]:
ctx.shlib(source=ctx.path.ant_glob('ext/'+plugin+'/*.c'), target=plugin)
You get cross-platform support for free. The code will create an 'a.dll' on Windows, 'a.so' on Linux and something else on OS X. Note that while the build function above looks like imperative code, it actually is not. Instead it is Python code to create a dependency graph which waf rebuilds for you when it is needed.Learn UNIX tools people, they’re excellent tools developed by masters with the amount of knowledge, experience and insight very few of you will ever amass (don’t think “ah! But I’m better!” for that way lie pain, misery and a build process of nightmares), and they work the way they do for multiple reasons. When it comes to build engines, Make is the real deal: sufficiently simple to get started right away, infinitely flexible to do whatever one needs when it comes to automated / intelligent building, with lots of useful built-in rules “out of the box”. In this case the problem isn’t in the tool, but in one’s own capabilities as a programmer.
See https://www.technovelty.org/tips/the-stamp-idiom-with-make.h...
Stamp files are an often an indication that someone is trying to turn a Makefile into a shell script.
Rules should be generated that generate a target from its dependencies. The intermediate steps should be encapsulated as the rule commands.
Building a static website where the targets are wildly different from the source files. I could probably make it explicit but it will be overly verbose and brittle.
Check for and remove trailing whitespace. Because the dependency is actually the target, a stamp file is necessary.
It has a bunch of extra features, like coverage and profiling and tags files. They are pretty easy to remove if you don't want them.
If you want to put object files to another directory, it's much better to run make in the output directory than to hack your Makefile to output objects to another directory. Make can look for source files elsewhere but not search for object files outside of $PWD.
I mainly use them to replace scripts to run tests/linters/formatters/containers/etc
But be aware, that as soon as you want to structure your project in any other way than it is the CMake default you are starting to write your own Makefile again ;-)
And doesn’t work with visual studio??? Whose problem is that (hint: not make’s).
The author has shown that he does not understand the Make language very well since half of his final bullet points are wrong.
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 should be simple, custom, and predictable for each piece of a project. I sometimes have a file for each dir. having a central invisible one is awful.