What makes samurai so much smaller? I’m not familiar with either codebase, but I would guess that Ninja has more complex optimizations for fast start up for massive builds. I vaguely recall reading something about lots of effort going into that.
I would guess that Ninja has more complex optimizations for fast start up for massive builds
In my personal benchmarks, samurai uses less memory and runs just as fast (or slightly faster) than ninja, even on massive builds like chromium. If you find a case where that’s not true, I’d be interesting in hearing about it.
As for why the code is smaller, I think it’s a combination of a few things. Small code size and simplicity were deliberate goals of samurai. As a result, it uses a more concise and efficient coding style. It also lacks certain inessential features like running a python web server to browse the dependency graph, spell checking for target and tool names, and graphviz output. samurai also only supports POSIX systems currently, while ninja supports Windows as well.
In some cases, samurai uses simpler algorithms that are easier to implement. For example, when a job is finished, ninja looks up whether each dependent job has an entry in a map, and if it does, it checks each other input of that job to see if it was finished, and if all of them are ready it starts the job. samurai, on the other hand, just keeps a count of pending inputs for each edge and when an output was built, it decreases that count for every dependent job, starting those that reached 0. This approach is thanks to @orib from his work on Myrddin’s mbld.
I have no bias either way, just curious.
As the author of samurai, I am a bit biased of course :)
If you find a case where that’s not true, I’d be interesting in hearing about it.
I don’t have any observations. It was just an off-the-cuff guess based on an article I read about ninja’s internals, and the possibility that you may have traded a little performance for simpler code. But on the contrary, your example of a simpler algorithm also sounds more efficient!
Thanks for the detailed reply. I didn’t even realize ninja had all those extra features, so naturally I can see why you’d omit them. I just installed samurai on all my machines! :)
In all honesty, I don’t find ninja much of an improvement. The syntax is a little bit more intuitive than make, but not enough to lure me into looking at it seriously.
I like the aesthetics of rake but requires ruby and gem which is a deal breaker, if you’re not a ruby shop. The make utility is omnipresent and Makefiles ubiquitous.
Is it really that complex to write Makefile for such simple project? And in many projects it can became even simpler as there are implicit rules in GNU Make for C/C++/TeX/etc.
Yeah, I was a bit surprised at Julia writing off make as arcane when she’s covered things like ptrace and kernel hacking elsewhere. Poorly documented is a better descriptor for make, and other people have done a decent job of fixing that.
Maybe she will see this and write another article on how make isn’t too arcane. :)
I’m (obviously) always interested in learning about things I thought were arcane and complicated are not actually that complicated! I’ve never read anything about make that I thought was approachable, but I haven’t tried too hard :)
I think the thing that throws me off about Make is that by default it seems to decide how to build something based on its file extension, and I find this pretty hard to reason about – what if I have 2 different ways to build PDFs files? (which I do).
I’m no Makefile expert, but the things with the file extensions are implicit rules and are basically there to save you from having to copy/paste the same basic rule template for each file. You can always override them with your own explicit recipe since those take precedent:
special.pdf: special.svg
do-this-command # indent via TAB!
then-that-command # ...
For simple one-off uses, I usually ignore all the details of the implicit rules and just spell everything out explicitly like that. I usually don’t even bother with automatic variables like $^ and $@ since I can never remember which is which. I’ll just copy/paste and search and replace all the recipes, or write a quick Python script to generate them. It’s hard to get much simpler than that. Just remember that the first target in the Makefile is the default if you type make with no targets.
I found the GNU Makefile manual to be very good about describing how make works.
The two different ways you make PDF files—do both methods use the same input files? If not, implicit rules can work just as well if the inputs are different (at work, I have to convert *.lua files to *.o files, in addition to *.c files to *.o).
FWIW I wrote 3 substantial makefiles from scratch, and “gave up” on Make. I use those Makefiles almost daily, but they all have some minor deficiencies.
I want to move to a “real language” that generates Ninja. (either Python or the Oil language itself :) )
Some comments here but there are tons of other places where I’ve ranted about Make:
I think my summary is that the whole point of a build system is to get you correct and fast incremental and parallel builds. But Make helps you with neither thing. Your makefile will have bugs.
I never condensed my criticisms in to a blog post, but I think that’s the one-liner.
I hear you, and I know that make is the standard make-type things are a bit more legitimate than the article I’m referencing.
That said, the amount of inexplicable boilerplate in something like this (from up in the thread) are why I’m super-empathetic to people who find Makefiles too complicated. And that’s ignoring things like the tab-based syntax or the fact that Makefiles effectively yoink in all of sh (or, honestly, these days, bash) as a casual comprehension dependency.
Makefiles can be very simple; I absolutely grant you that. They generally aren’t. And even when they are, they require a certain amount of boilerplate that e.g. ninja does not have.
Shake took me a little while to get my head around, but it’s really cool. Wound up baking it into an internal tool at work, and replaced a lot of makefiles and shell glue.
I love using Ninja for build systems. I use it for Javascript and website building, more recently I converted our 3d-printed open source microscope build over to it.
I feel like the use-cases Ninja was designed for are not the same as what I want to use it for, but it’s actually quite well suited because of it’s simplicity and focus on generating the build file. I wish it did input file hashing instead of reading timestamps as an option. That would be really useful for caching CI builds. Someone implemented it but the patch was rejected. I think because with C/C++ it’s a solved problem. I am considering maintaining a fork with that feature now (read more if you want).
Oh that’s cool … Did you see this post a few months ago? The authors were describing a system that used Ninja and content hashes (via OSTree). I have wanted to play around with it, but haven’t gotten around to it.
For custom written rules, I personally find that nothing beats the simplicity of redo. The build system is language agnostic in that you can execute a build step in any language and redo only tracks what needs to be rebuilt. For the example at hand, you could write the conversion rule as (in a file named default.pdf.do):
svg2pdf $2.svg $3
Then by calling redo-ifchange *.svg the entire process would be taken care of.
I would recommend using rsvg-convert -f pdf $2.svg > $3 in the example above. It is a much faster way to convert svgs to pdfs. If I remember correctly, it’s part of librsvg, an svg library written in rust.
What implementation are you using? If I were going to use Redo, I would want to keep it in-tree because it’s not nearly as widely deployed as Make/Ninja.
One of the nice things about redo is that there’s a single-file pure-shell implementation that only knows how to call the all build-scripts in the right order, which is great for shipping to end-users so they can compile it and move on with their lives.
Meanwhile, developers who are invested enough to build your software multiple times can install redo, which does all the incremental-build and parallelisation magic to make repeated builds efficient.
Hot take: a frankenstack of Python generating Ninja generating commands that execute in a shell is not simple.
Nothing against Ninja. This blog post on its design is great. But it’s enormously subtle. The fact that Chrome and other large projects use it should be a warning to most projects, not an encouragement.
Why, though. Why is this a good idea? Why is it a better idea than generating a Makefile with Python?
Introducing unnecessary dependencies is the essence of a frankenstack, to my mind. Even if you built your Ninja generator in machine code, I’d wonder why.
There are certainly good reasons in context. Sometimes we find ourselves backed into a situation with existing Ninja files. And large projects have a way of bending spacetime in their neighborhood. But it’s always something to undertake with regret. I don’t understand this valorization of Ninja generators.
If you don’t want to use Make, either for philosophical or technical reasons, this is a fine idea. If you have more complex build requirements but don’t want to use CMake, this is a fine idea.
If you live in a universe where the only options are to use make or cmake or ninja. Or to stab yourself repeatedly in the face.
My claim: start with the assumption that it’s a stupid, lousy, no-good idea. Then go ahead and do it if all the alternatives are worse, based on your philosophical or technical reasons. And don’t ever call it a fine idea.
There are a lot of universes where it does not make sense to pay someone to rewrite a good build tool, and this is what ends up happening in those universes.
Final note for posterity: there was a miscommunication here that we identified offline. To clarify, I am criticizing overuse of Ninja generators, not Ninja itself.
Why, though. Why is this a good idea? Why is it a better idea than generating a Makefile with Python?
In my experience with using cmake on a middle-sized project, the generated build.ninja is significantly faster than the Makefile at incremental builds.
That totally makes sense! The blog post I linked to also discusses performance. It’s a far superior framing to think of Ninja generators as an optimization, paid for with some (bounded) additional complexity.
There’s also the ninja-compatible samurai. Written in C instead of C++, and significantly less of it.
What makes samurai so much smaller? I’m not familiar with either codebase, but I would guess that Ninja has more complex optimizations for fast start up for massive builds. I vaguely recall reading something about lots of effort going into that.
I have no bias either way, just curious.
In my personal benchmarks, samurai uses less memory and runs just as fast (or slightly faster) than ninja, even on massive builds like chromium. If you find a case where that’s not true, I’d be interesting in hearing about it.
As for why the code is smaller, I think it’s a combination of a few things. Small code size and simplicity were deliberate goals of samurai. As a result, it uses a more concise and efficient coding style. It also lacks certain inessential features like running a python web server to browse the dependency graph, spell checking for target and tool names, and graphviz output. samurai also only supports POSIX systems currently, while ninja supports Windows as well.
In some cases, samurai uses simpler algorithms that are easier to implement. For example, when a job is finished, ninja looks up whether each dependent job has an entry in a map, and if it does, it checks each other input of that job to see if it was finished, and if all of them are ready it starts the job. samurai, on the other hand, just keeps a count of pending inputs for each edge and when an output was built, it decreases that count for every dependent job, starting those that reached 0. This approach is thanks to @orib from his work on Myrddin’s mbld.
As the author of samurai, I am a bit biased of course :)
I don’t have any observations. It was just an off-the-cuff guess based on an article I read about ninja’s internals, and the possibility that you may have traded a little performance for simpler code. But on the contrary, your example of a simpler algorithm also sounds more efficient!
Thanks for the detailed reply. I didn’t even realize ninja had all those extra features, so naturally I can see why you’d omit them. I just installed samurai on all my machines! :)
Any chance fixing this and adding Windows?
In all honesty, I don’t find
ninja
much of an improvement. The syntax is a little bit more intuitive thanmake
, but not enough to lure me into looking at it seriously.I like the aesthetics of
rake
but requiresruby
andgem
which is a deal breaker, if you’re not a ruby shop. Themake
utility is omnipresent andMakefiles
ubiquitous.Is it really that complex to write
Makefile
for such simple project? And in many projects it can became even simpler as there are implicit rules in GNU Make for C/C++/TeX/etc.The makefile is simple, but knowing which incantations to use to make it simple is harder.
Yeah, I was a bit surprised at Julia writing off
make
as arcane when she’s covered things likeptrace
and kernel hacking elsewhere. Poorly documented is a better descriptor formake,
and other people have done a decent job of fixing that.Maybe she will see this and write another article on how make isn’t too arcane. :)
I’m (obviously) always interested in learning about things I thought were arcane and complicated are not actually that complicated! I’ve never read anything about make that I thought was approachable, but I haven’t tried too hard :)
I think the thing that throws me off about Make is that by default it seems to decide how to build something based on its file extension, and I find this pretty hard to reason about – what if I have 2 different ways to build PDFs files? (which I do).
I’m no Makefile expert, but the things with the file extensions are implicit rules and are basically there to save you from having to copy/paste the same basic rule template for each file. You can always override them with your own explicit recipe since those take precedent:
For simple one-off uses, I usually ignore all the details of the implicit rules and just spell everything out explicitly like that. I usually don’t even bother with automatic variables like
$^
and$@
since I can never remember which is which. I’ll just copy/paste and search and replace all the recipes, or write a quick Python script to generate them. It’s hard to get much simpler than that. Just remember that the first target in the Makefile is the default if you typemake
with no targets.I found the GNU Makefile manual to be very good about describing how
make
works.The two different ways you make PDF files—do both methods use the same input files? If not, implicit rules can work just as well if the inputs are different (at work, I have to convert
*.lua
files to*.o
files, in addition to*.c
files to*.o
).FWIW I wrote 3 substantial makefiles from scratch, and “gave up” on Make. I use those Makefiles almost daily, but they all have some minor deficiencies.
I want to move to a “real language” that generates Ninja. (either Python or the Oil language itself :) )
Some comments here but there are tons of other places where I’ve ranted about Make:
https://www.oilshell.org/blog/2017/10/25.html#make-automatic-prequisites-and-language-design
I think my summary is that the whole point of a build system is to get you correct and fast incremental and parallel builds. But Make helps you with neither thing. Your makefile will have bugs.
I never condensed my criticisms in to a blog post, but I think that’s the one-liner.
I hear you, and I know that
make is the standard make
-type things are a bit more legitimate than the article I’m referencing.That said, the amount of inexplicable boilerplate in something like this (from up in the thread) are why I’m super-empathetic to people who find
Makefile
s too complicated. And that’s ignoring things like the tab-based syntax or the fact thatMakefile
s effectively yoink in all ofsh
(or, honestly, these days,bash
) as a casual comprehension dependency.Makefile
s can be very simple; I absolutely grant you that. They generally aren’t. And even when they are, they require a certain amount of boilerplate that e.g.ninja
does not have.That’s about how I’d write it for a good hand-written yet scalable Makefile, but it’s worth noting that you can go even simpler still here:
Easy enough that you can do it from memory. This is also the style that I go for when writing a quick script to generate a Makefile.
Shake can also consume ninja build files, despite also including its own (more powerful) build language.
Shake took me a little while to get my head around, but it’s really cool. Wound up baking it into an internal tool at work, and replaced a lot of makefiles and shell glue.
I love using Ninja for build systems. I use it for Javascript and website building, more recently I converted our 3d-printed open source microscope build over to it.
I feel like the use-cases Ninja was designed for are not the same as what I want to use it for, but it’s actually quite well suited because of it’s simplicity and focus on generating the build file. I wish it did input file hashing instead of reading timestamps as an option. That would be really useful for caching CI builds. Someone implemented it but the patch was rejected. I think because with C/C++ it’s a solved problem. I am considering maintaining a fork with that feature now (read more if you want).
Oh that’s cool … Did you see this post a few months ago? The authors were describing a system that used Ninja and content hashes (via OSTree). I have wanted to play around with it, but haven’t gotten around to it.
https://lobste.rs/s/952gdv/merkle_trees_build_systems
For custom written rules, I personally find that nothing beats the simplicity of
redo
. The build system is language agnostic in that you can execute a build step in any language and redo only tracks what needs to be rebuilt. For the example at hand, you could write the conversion rule as (in a file nameddefault.pdf.do
):Then by calling
redo-ifchange *.svg
the entire process would be taken care of.I would recommend using
rsvg-convert -f pdf $2.svg > $3
in the example above. It is a much faster way to convert svgs to pdfs. If I remember correctly, it’s part of librsvg, an svg library written in rust.What implementation are you using? If I were going to use Redo, I would want to keep it in-tree because it’s not nearly as widely deployed as Make/Ninja.
One of the nice things about redo is that there’s a single-file pure-shell implementation that only knows how to call the all build-scripts in the right order, which is great for shipping to end-users so they can compile it and move on with their lives.
Meanwhile, developers who are invested enough to build your software multiple times can install redo, which does all the incremental-build and parallelisation magic to make repeated builds efficient.
Do you have a link?
Not your parent commenter, but maybe they meant https://github.com/apenwarr/redo/blob/main/minimal/do
Aha! Thanks. Currently I am using a build script by our very own @akkartik, which has worked for me so far:
I’m a huge fan of BSD Make. Here is a sample Makefile from a project I maintain called
libpushover
:Hot take: a frankenstack of Python generating Ninja generating commands that execute in a shell is not simple.
Nothing against Ninja. This blog post on its design is great. But it’s enormously subtle. The fact that Chrome and other large projects use it should be a warning to most projects, not an encouragement.
You don’t need a frankenstack! After seeing this post I wrote a small Ninja generator in Python and it worked really well. Take a look: https://git.sr.ht/~max/ghuloum/tree/master/build.py
Why, though. Why is this a good idea? Why is it a better idea than generating a Makefile with Python?
Introducing unnecessary dependencies is the essence of a frankenstack, to my mind. Even if you built your Ninja generator in machine code, I’d wonder why.
There are certainly good reasons in context. Sometimes we find ourselves backed into a situation with existing Ninja files. And large projects have a way of bending spacetime in their neighborhood. But it’s always something to undertake with regret. I don’t understand this valorization of Ninja generators.
If you don’t want to use Make, either for philosophical or technical reasons, this is a fine idea. If you have more complex build requirements but don’t want to use CMake, this is a fine idea.
If you live in a universe where the only options are to use
make
orcmake
orninja
. Or to stab yourself repeatedly in the face.My claim: start with the assumption that it’s a stupid, lousy, no-good idea. Then go ahead and do it if all the alternatives are worse, based on your philosophical or technical reasons. And don’t ever call it a fine idea.
There are a lot of universes where it does not make sense to pay someone to rewrite a good build tool, and this is what ends up happening in those universes.
Final note for posterity: there was a miscommunication here that we identified offline. To clarify, I am criticizing overuse of Ninja generators, not Ninja itself.
In my experience with using cmake on a middle-sized project, the generated build.ninja is significantly faster than the Makefile at incremental builds.
That totally makes sense! The blog post I linked to also discusses performance. It’s a far superior framing to think of Ninja generators as an optimization, paid for with some (bounded) additional complexity.
For simple builds, I like using tup. The syntax is light, and it’s very fast to build (I also like the progress output).
Documentation could be better though.
Should languages just specify their build systems?
[Comment removed by author]