98 comments

[ 25.7 ms ] story [ 1949 ms ] thread
> Most part of my career I used Visual Studio and when switched to Linux I was slightly shocked. Compared to MSVS, makefiles felt like bows and arrows against machine gun.

I honesty have no idea how some people come up with these absurd comparisons. I mean, who in their right mind feel is appropriate to compare a Makefile with a build system? And how clueless about things are "in the Linux world" do you need to be to claim that Makefiles are how people build software in Linux?

I mean, I've been using Linux for a couple of decades now and I can't remember the last time I saw a project that relied on Makefiles to build. At best projects use some kind of Makefile generator. The ancient ones use autotools and even those are going the way of the dodo.

> CMake allows us to handle almost all those things right except one - it doesn’t have return values

This is a red flag. If you're hoping to write functions with cmake... You're doing it wrong. Very wrong. You add targets, and you add rules to the target. If you find yourself wanting to do anything other than this then you should take a step back and read a modern cmake tutorial again, because odds are you're doing it wrong.

> I've been using Linux for a couple of decades now and I can't remember the last time I saw a project that relied on Makefiles to build.

Makefiles, at least in my corner of the industry, are exceedingly common for projects with a build process of medium-to-low complexity.

Link to a free and open modern cmake tutorial?
You can pretty much google for "modern cmake tutorial" to get a long list of tutorials on how to write target-based cmake projects.
spoiler: they're all crap.
And worse, they all achieve the same goal in different ways...
Not to mention, target outdated ways of doing things with CMake (there is always 3-4 at least).
A great point. There should a single standard and well-documented way to do things. The documentation should be official, the user should not be expected to trawl the blogosphere and piece together the knowledge they need. There should be plenty of complete examples.

Ideally the official Find... packages should not make use of deprecated CMake functionality, they should be actively maintained to follow the current best-practices, so that they can serve as examples of the right way to do it.

As I rambled about in my longer comment yesterday, this kind of work shouldn't require you to think, it should just be an exercise in applying boring, samey, dependable boilerplate.

I would love to use makefiles and git submodules instead of this msbuild/nuget contraption. That would be way less sophisticated, but I would welcome it. msbuild seems like a good example of microsofts tendency to overengineer simple things. But sometimes its suprisingly simple. different build configuration are just strings stiched together like "x64|Debug", and then are are conditionals to that value. Msbuild it's kind of a CLR language, but you can't debug it. The xml sytax and the syntax you use in the attributes feels arkward but sometimes limited for no good reason. Is <Message Text="Backup files: @(Compile->'%(filename).bak')" /> what i would want, if I'm used to linq? No, not me. @ and % are not intuitive for me. I usually have to try out what works. Accessing members of objects is odd. Calling System.String.Format feels like I'm supposed to do it. Anything interesting has to be done with custom task, and these are hard to debug aswell. For example studio would unload the .dlls, so you have to close it every time. That thing feels like a classic turing tarpit. A very complicated turing tarpit.

Then again, if your build should output a .nupkg, the whole infrastructure seems to be lacking. You are left alone doing things that seem like they should be part of a package manager. e.g. handling of the version number. The whole build system knows the build output, but you have to specify it by hand again, in a different file.

(comment deleted)
> I can't remember the last time I saw a project that relied on Makefiles to build

I don't know what GNU/Linux you're using, because in my experience, most packages going into a distro use make.

Oh, and then there is that whole denominator in the GNU/Linux fraction that's built with GNU Make.

Maybe you don't see Makefiles because you're relying on binary packages?

> I don't know what GNU/Linux you're using, because in my experience, most packages going into a distro use make.

no, they use autotools or cmake, which happens to generate makefiles. the package authors themselves definitely don't write any makefiles by hand except maybe for trivial packages (and frankly I'm not sure)

Autotools generate a Makefile project, and nothing but a Makefile project. All the buiding in the development cycle is done by typing "make". Autotools projects include a template makefile: Makefile.am. Make is not concealed in any way.

Also note that not all Autotools-based projects use Automake. Using only Autoconf, with a hand-maintained Makefile, is a thing.

The goal of Autotools is to produce a software distribution that doesn't require the end user to have Autotools. The end user has a shell to run the configure script, and that's it. If that works, they run "make".

About the most sophisticated generation the user will see is that some Makefile.in template is passed through sed to substitute @@XYZ@@ variables.

This is true for Autotools projects, but isn't true for CMake, Bazel, Meson, and others. Let's take CMake for example, which is IMHO the most popular build tool: most modern CMake projects prefer to use the Ninja build system over Make.

And coming back to Autotools, some of the largest Autotools projects (see the GNOME ecosystem) has been moving/moved to Meson, which can also be used to generate Ninja rules instead of Makefiles.

CMake Generators: https://cmake.org/cmake/help/latest/manual/cmake-generators.... Meson: https://mesonbuild.com/ Bazel: https://bazel.build/

GNOME and Meson: https://wiki.gnome.org/Initiatives/GnomeGoals/MesonPorting

> Autotools generate a Makefile project

That's what all build systems do. Hell, cmake's description is "cross-platform Makefile generator".

The rationale is quite obvious: the DAG part is solved problem. The hard part is extracting platform-dependent info, perform validations, and use it to generate the DAG.

And a Makefile is a DAG.

Make is like an assembly language of the build systems. This layer gives structure and debuggability (otherwise you're spending time poking at some opaque UI or something).
I use modern cmake as much as I can, but I often run into things that cmake cannot do without a function. Most of our custom code generation tools for example, cmake doesn't know how to turn them into c++ without a function (which is in turn a wrapper of add custom command /target

I've gotten rid of several of our pre modern cmake functions, but more remain.

> This is a red flag. If you're hoping to write functions with cmake... You're doing it wrong. Very wrong. You add targets, and you add rules to the target. If you find yourself wanting to do anything other than this then you should take a step back and read a modern cmake tutorial again, because odds are you're doing it wrong.

so how do you propose to do code generation, parsing stuff like /etc/lsb_release to work around a bug for people building under ubuntu 16.04, or that kind of thing ?

A semi-relevant rant about CMake, for those interested in that kind of thing:

The CMake scripting language is just awful. It breaks with convention to do things in its own unintuitive way. (Who though the endfunction() syntax was a good idea? Or the bizarre function-call syntax?) It's error-prone, it shows cryptic error-messages, it's able to break in subtle ways, much of the documentation is for deprecated functionality, and worst of all, it lacks a consistent design pattern. One shouldn't have to think when making a Find... CMake script so that CMake can find an installed package. It should be a very boring task of adapting existing boilerplate. But it's not so. It's an agonising uphill battle with this atrocious language.

Subtle bugs like [0] wouldn't even be possible in a well-designed build-system, but CMake lets you shoot yourself in the foot as punishment for not knowing its every subtlety.

It's unfortunate that CMake is the least bad build system we have. You should probably still use CMake for multi-platform code. You shouldn't expect people to learn some obscure rival build-system just to work on your code. I'll certainly continue using CMake for what little C++ I write myself. Once you've beaten your CMake scripts into submission (generally thanks to the extensive body of knowledge on StackOverflow, in my experience), CMake works a treat on a bunch of different platforms. It's just a pity the language is so terrible.

[0] https://github.com/dankamongmen/notcurses/issues/711

>It's unfortunate that CMake is the least bad build system we have. You should probably still use CMake for multi-platform code. You shouldn't expect people to learn some obscure rival build-system just to work on your code.

What little C I write I compile with Makefiles, but I've seen many new C projects on Github using meson. It look better than cmake at a glance, and at this point in time isn't obscure either.

Thanks for the pointer to meson. Ten minutes into it and it's already far less broken than other build systems I've been test driving recently. I'm sure it will let me down soon, but so far ... :-)

(My reaction to CMake's scripting language was unprintable).

Meson is quite good. It's biggest issue is it is not Turing complete so it has certain limits of extensibility.
CMake is a meta programming language designed to compile into Makefiles.

It can compile into visual studio projects or xcode projects. That's how it achieves multiplatform support.

Basically by writing it in CMake rather then make you've automatically written your make file as well as many other platform configurations. Of course the caveat like the OP is that CMake syntax and design decisions are just plain bad.

I think you replied to the wrong comment.
I don’t think so. The parent was saying that Make isn’t a replacement for CMake because it isn’t multi platform.
(comment deleted)
They replied to a comment mentioning meson as a replacement for cmake by explaining how cmake works.
Meta-build systems need a clean separation between variables computed by the program (or cached), and parameters set by the user. Placing both in a single CMakeCache blob causes confusion and nasty semantics when local set() variables, option() variables, and command-line -DKEY=VALUE collide.
> It's unfortunate that CMake is the least bad build system we have.

With caveats--I've switched to Bazel. Bazel does solve the very real, very hairy problems of compiling C++ programs for different platforms, on different platforms. It is a damn beautiful thing. There are a few places where Bazel really shines. For example, generated sources, multi-language projects, and building third-party dependencies from source (without monorepos or Git submodules--you can get your third-party sources from HTTP, Git, etc).

If my project does any of these things (third-party dependencies, uses multiple languages, or generated sources), I reach for Bazel immediately.

That said--it is also impossible to give an unqualified recommendation for Bazel. The documentation and community is catching up, and a few simple things (like using pkg-config) are a bit more difficult to get working than they should.

Bazel is the least bad build system I've used too. Just wanted to note, though, it is not damn beautiful. Maybe its internal (idealistic) model is beautiful, but the outer parts are something else. It does have awesome features, but it has lots of bugs, rough edges, and weird behavior too. Try specifying your own compilers, or adding your own steps to the build process, getting things working on Windows, etc. and if you don't already know how to do it, you will probably hate your life trying to figure it out. And forget about putting your workspace into a path with spaces, even if your repo path lacks spaces inside it and you use relative paths everywhere -- it simply can't do that (while even Make can). There are lots of random issues with undeclared headers and ungenerated files that come up from time to time that make absolutely no sense and are likely bugs that are incredibly hard to track down. (Of course some of these errors are legitimate too, just painful to track down. I'm saying not all of them are. Some are even nondeterministic.) And good luck if your user name (and hence home directory) has a space in it, which it very well might if you're on Windows. All in all, it might be better than anything else I've used, but not in every case, and that's not an unconditional endorsement.
While Make can deal with spaces, you have to pass them through multiple layers of splitting. I've done it before, but I've given up on anything besides escaping every single space and pasting it into the Makefile (with a script), and not using any of Make's variable expansion functionality.

Spaces are how Make separates items in arrays, after all. For example, take this contrived example:

    foo.c\ bar.c
            echo $(patsubst %.c,%.h,$@)
If Make had some concept of arrays, it would echo "foo.c bar.h", but it does not. While this example is super contrived, it should illustrate the general problem.
Note I was explicitly talking about spaces in the current directory (which is only dependent on where the user clones the repo), not spaces in the workspace (which depends on what's in the repo). Your examples are about the latter. The latter isn't as big of a deal because the repo owner at least has control over the contents and can avoid putting spaces in file names (and use relative paths everywhere). But they can't control where the user clones their repo.
Ah, that makes sense. Maybe I've just suffered abuse from projects that can't be built anywhere for so long that it seems normal--I remember a particular team I worked on where the project needed to be spread across the X:, Y:, and Z: drives with bind mounts.
I was compiling emacs with mingw and its Makefiles broke with spaces in the path. So while Make can, it seems that few people can get it right.
Yeah, I don't have a lot of faith in UNIX-oriented tools in getting it right, because they just don't care. But at least it's possible to get it right if you care to; you just use relative paths instead of absolute paths, and avoid spaces in your relative paths (which you can control). With Bazel, it's literally impossible to do that.
> And good luck if your user name (and hence home directory) has a space in it, which it very well might if you're on Windows.

I went to great lengths on my recent Windows install to ensure my user profile, which has my real name in it, uses `efreak` as the name for my home folder specifically to deal with software that hates having spaces in filenames. There might well be a way to specify this in Windows, but I'm not aware of it. Instead, I had to recreate my user profile 3 times due to Microsoft syncing; first it wanted 'first last', then it wanted my Microsoft account name. I don't recall how I got it working finally; I may have renamed the account before signing into sync (it keeps my desktop environments together across several computers). Using 8.3 filenames can help too, but that requires remembering what they are.

If you've only got a single folder in the path that has a space in it, then I've had success using symlinks (or junction points on windows, since explorer recognizes those and doesn't recognize symlinks as such). Just create a folder c:\mounts or ~/symlinks to work with.

And god forbid you ever have to work with the qt build environment in Windows, extracting and deleting the source each take like 10 minutes on my ssd... I really wish it was distributed as a vhd file, or that I could convert it to one easily.

+1 for Bazel. It is most importantly a language independent package overlay. To put it simply, it pretty solves the package/module issues for C++, as well as cross compilation etc...

It's beautiful.

>generated sources, multi-language projects, and building third-party dependencies from source

What is special about how Bazel does these? I have never had a problem doing any of this in CMake, or Meson.

In my experience the only reason to pick any over the other is because they tend to vary wildly in supporting certain compiler toolchains for non C/C++ projects.

Bazel generates reproducible builds by default and is very cache friendly. It forces hermetic build and test environments; each build/test gets it's own sandbox that only contains and preserves deps and outputs.

It's not magically different than CMake and Meson, it just works in a way that I never every think "maybe make clean will fix this".

I can trust it to work regardless of the changes.

You can also, out of the box, use remote build/test execution & remote caching shared between Devs and CI.

It's also VCS aware and uses that to help prune down change sets to make builds faster.

> It forces hermetic build and test environments;

how would that ever work if you want to build software to be packaged in a linux distro where your software has to comply with $VERSION of every library provided by the distro ?

It is focused on producing static (or mostly static) binaries. Obviously not everything can be in the sandbox, but in the Bazel world, it takes over the build of all your dependencies (as it is also a package overlay).
Bazel isn't really designed for dynamic linking applications. You can include system libraries if you need:

https://groups.google.com/g/bazel-discuss/c/Ndd820uaq2U

You'd also need to make sure everything is marked as `linkstatic=false`, but I'm not sure if that would work correctly.

But, in the end of the day, you're going against the grain of bazel here. Bazel expects to statically link everything to make distribution to heterogeneous environments simpler.

right, but then understand that if someone wants to create a software for which a goal is to be packaged & distributed by e.g. Debian, homebrew, msys2, Fedora, CentOS..., this is an absolute non-starter.
Haven't used Meson, but if you know a nice way to cross compile a project with generated source I'd like to know.

More specifically, I'd like the following things happen:

1. Compile flatc with the host compiler since we have to run it later, the FlatBuffers schema compiler which compiles .fbs schemas to .cpp, just like protoc of protobuf. 2. Run the flatc on all my .fbs schemas. 3. Compile the output of 2 with the cross-compiler for the target.

With Bazel it can automatically detect the fact that it is going to run the flatc on the same box so it would be compiled with the host compiler. With CMake it seems impossible since it does not have a concept of "build host", so my current solution is to add the FlatBuffers repo via ExternalProject_Add twice, once for host configuration with a patch to change its CMake target namespace prefix to "flatbuffers_host" (because apparently this is not possible to override later) to avoid name collision, once for the target configuration, then change every reference of flatc in my project to "flatbuffers_host::flatc".

> you can get your third-party sources from HTTP, Git

Can you elaborate? I assume those projects have to be brought into the Bazel configuration format, or does it have some support for foreign build systems?

It doesn't, at least not in any form you'd want to rely on. Bazel makes it easier than CMake to pull in externals in source form, but you'll still want to write Bazel build rules for them in most cases. In practice for C/C++ externals being built in a Bazel project, almost all of the build can be captured by a couple

    cc_library(...)
and

    cc_binary(...)
rules.
I've had kind of a crazy idea of building an entire Linux-based embedded system using Bazel. Based on your previous comment, I briefly thought they might have had some detection of foreign build systems now.

Some part of me still finds the idea kind of interesting. The idea of doing a Linux-from-scratch, but porting each project to Bazel in the process. It's kind of silly and would be a lot of work, but I think about being able to modify a header of any library in the system, and have that change quickly percolate up to every dependent application automatically.

The way to do it now is through bitbake, but it (rightly) is just wrappers around every other build system. The goals are a bit different than Bazel's, but it's quite slow.

Came here to say just this. It's a marvel of engineering and the slack for the community is really helpful. I hope more people join in the future.
In my experience with several Bazel projects, when everything works, it's great.

But, the case where it doesn't work are way more painful than anything I've encountered from CMake + (make, ninja). Bazel is basically pathologically bad about resource use - if you run out of anything (CPU, memory, disk, inodes) you're totally hosed and probably have to reset the machine. Bazel caching is great, but boy do you pay for it in disk space - the bazel cache on my machine, for a reasonable (but by no means huge) monorepo is probably more than 100GB, and because it absolutely loves creating symlinks to everything it's very easy to run out of inodes on smaller disks even when there's plenty of space otherwise available.

Bazel basically feels like the "GC language" of build systems - it behaves like there's nearly infinite resources available and so long as that illusion can be maintained, all is well. For reference, the machines I'm using are plenty beefy (several AMD 16C/32T + 128GB ram + 1TB nvme, and several Intel 28C/56T) - it's really inexcusable that a build system makes the machine hang.

Just curious, what specific project do you compile so that you ran out of resources in that machine?
The project is our internal monorepo, with probably order ~1000 targets, mostly C++, with reasonably heavy use of templates. GCC needs somewhere on the order of 2-4GB to compile each C++ target.
> inodes

Can't you deal with this by using a mounted filesystem?

Oh sure, it's fixable and something you can work around, but having your build system responsible for "machine is unresponsive and acts like it's out of disk but you've got >100GB space remaining" is a real pain in the ass. Running out of inodes (and specifically, that you can run out of them well before running out of disk space otherwise) is also really not an obvious failure mode to most people.
I find it remarkable how rarely Find... CMake scripts actually work, even if they're shipped by the distro (especially if they're shipped by the distro). Also, on !Linux CMake scripts tend to require at least one manual variable change per dependency.
Most Find... scripts are just hoping you have the dependency installed in some default location.
And it works with Linux distributions.
"It's unfortunate that CMake is the least bad build system we have"

I think build systems (and packaging and installers and...) are sort of like doing your own taxes.

Things start out simple, and but at some point things become complicated and you want to just get through it and get out and forget about it until next year. (just pay someone else to do it!)

It's really too bad, I think in the software development process, there are some friction points that if they just had a little lubrication, would make everything go so much faster.

I suspect these sorts of things aren't "sexy" like machine learning, or game programming or writing a new language. But things that are important don't always correlate that way.

> CMake lets you shoot yourself in the foot as punishment for not knowing its every subtlety.

Seems adequate for the somewhat-de-facto standard for C++ projects!

Im glad it's not just me. I've spent weeks learning basic C to contribute a few patches to my favorite OSS project.

I swear it's taken me longer to figure out how to build and include libraries with CMake than learn C. And it still makes no sense to me. Seriously, what the fuck is wrong with C/C++ package management.

I gave up on learning the build system. I recreated a working build setup in Code::Blocks UI then exported it to a makefile. Then exported that to CMake and copy pasted shit into the original build setup until it worked. Fuck it. C library management is harder than the language

Yes, everyone on HN likes to make fun of JavaScript because of leftpad, but the C/C++ ecosystem is much, much worse, which is a real achievement.
No, C/C++ is straightforward when you stick to a distribution because there are people who are dedicated to packaging and deal with inconsistencies that individual library developers love to multiply.
And anyone who tries to build your software on another distribution will feel the pain.
> I swear it's taken me longer to figure out how to build and include libraries with CMake than learn C

I had a similar experience learning C++. Learning the C++ language isn't all that straightforward, but there are at least plenty of resources available. Build-systems felt much more like a black-box, until I'd hit them with a stick for long enough that they felt familiar.

I started out with single-file C++ programs, invoking g++ manually with the appropriate flags for include directories and linking. I think that's probably the best way to go. Work up to proper build systems a little later on.

Perhaps another good way to start off would be to just use an IDE, and ignore the build process while you're starting out.

...it gets worse: it was born to manage a medical imaging project[0][1] that was already using a proper, regular embedded scripting language. Then they decided “we need a build system - let’s not use the battle-hardened proven language we’re already using in this very project we’re working on”.

I always feel a pang of guilt complaining about cmake, because people made it, certainly with good intentions, and it is an engineering feat... but to my mind passing over Tcl for what was created was a huge mistake. I tried to love CMake for years; turns out I’m happier writing a plain-Jane Makefile.

[0] https://vtk.org/

[1] https://en.wikipedia.org/wiki/VTK

I am not by any means a tcl fan. But after years of banging my head against cmake I would probably prefer a tcl build system.

Scons never quite found a critical mass. I suppose it had its own quirks but I think I'd have preferred it over cmake too.

I recently discovered unity builds, only applied to C, and I love the quick build times. 10,000 lines of code, included directly into main from other .c files, compiles in 1 second on my machine with -01 optimizations
Unity builds are a neat idea, but they just don't scale. One of my projects is about 3k loc (for the time being...)

Takes 1.6s to build right now, under clang. (Amazingly, it takes 68ms with tcc, but that's a separate issue.)

If I switch to a parallel build, that time halves to only 0.8s. You can't do unity builds in parallel, because there's only one translation unit. Most people will probably get much better than 2x performance from parallel builds; I only have 4 cores.

You also can't do incremental builds in unity. If I touch just my biggest file (so only that has to be rebuilt), my compile time halves. Touch the next-biggest file, and it halves again. As a project grows, time it takes to do a full rebuild gets bigger, but incremental builds take about the same amount of time. (They may take slightly longer, as your headers grow or as the linking stage takes more time.)

You may have slightly better compile times right now, but they won't last. Incremental, parallel builds still have problems (especially when you get to the scale of linux or chromium), but but there's a reason few people use unity builds.

Fortunatley I've never surpassed 15,000 lines on a personal project!
Still, though, your incremental builds will be much faster if you avoid unity builds. Full rebuilds are rare (assuming your build system knows about header dependencies, which it should).

Unity builds also make it hard to do abstraction or information hiding well. There's a form of polymorphism in c I describe at [1] which requires separation of translation units.

1. https://comp.lang.c.narkive.com/aa5pZoND/polymorphic-oop-hac...

True, but I typically build with link time optimizations (-flto) since I do a lot of software rendering, and this really hurts incremental builds too. I'll checkout your link, but as for polymorphism I've had good luck with doubly linked lists holding void pointers, and routinely writing maps / filters with

    #define foreach(node, list, ...) for(Node* node = list->head; node; node = node->next) __VA_ARGS__
And using it along the lines of

    void Move(List* units)
    {
        foreach(node, units, {
            Unit* unit = node->data;
            unit->position.x += unit->speed.x;
            unit->position.y += unit->speed.y;
        }
    }
Or something along the lines of that
> link time optimizations

Ah, fair enough.

(Personally, I don't generally find that the performance delta between having lto on|off is worth the compile time. Maybe for release builds. But usually performance is jut not that much of a concern; I don't bother.)

  void Move(List* units)
  {
      foreach(node, units, {
          Unit* unit = node->data;
          unit->position.x += unit->speed.x;
          unit->position.y += unit->speed.y;
      }
  }
My approach doesn't enable anything fundamentally different from this. But it makes it more ergonomic and safer.
I see your point with opaque pointers though. They are great for code obfuscation when shipping libraries, and totally do require a header / source file split making unity builds not an option.

It always amazes me how C can reach the height of most modern languages with a bit of (serious) effort.

> CMake is the least bad build system we have.

Hell, no. If you do C or C++ (with no or a small set of dependencies) then a portable makefile is certainly easy and beautiful to write.

(comment deleted)
Raw makefiles aren't good enough. I want cross-platform package-detection and Visual Studio support.

Autotools is the Unix-only solution to package-detection + environment-detection + build. The cross-platform solution is CMake (not counting its various obscure and immature competitors).

> Autotools is the Unix-only solution to package-detection

I do not understand the concept of "package detection".

I guess you mean about dealing with dependencies that are not copied into your code. In that case, either your dependency is correctly installed into your system (and then, by definition, it is automatically found by the compiler and the linker), or it is not. If your dependecy is not correctly installed, either the compilation will fail (which is correct behavior), or you will have to manually edit the configuration so that it can be found, by specifying its path (which is also correct behavior). Both of these things can be perfectly done with plain makefiles. No need for autotools (which is shit) nor cmake (which is shit squared).

The alternative is to have your build system to search around your disk for possible versions of the required dependency. This is a completely untoward behavior and in very bad taste.

> I do not understand the concept of "package detection".

The detection of software packages depended on by my code. Typically this means libraries, and perhaps also executables, either executables needed for the build process (such as code-generators) or executables that will be needed at runtime.

If my code uses a Boost library, the Autotools configure script (or CMake) can check that Boost is installed on the machine, check that it has an acceptable version number, and configure the build to look in the right place to find Boost's header files. If Boost is not installed, the configure script (or CMake) will fail with an error. It will do this quickly, not part-way through a build, which is poor form and can be annoying for the user.

Configure scripts can also handle optional dependencies. They can also detect platform-specific attributes and produce a header file of the values, helping the developer cope with the platform's quirks. (It might check for the availability of a compiler intrinsic, or something of that sort.)

> In that case, either your dependency is correctly installed into your system (and then, by definition, it is automatically found by the compiler and the linker), or it is not.

There is no canonical installation path for a Unix package. You're right that the linker can be expected to, well, handle the linking, but that's not the whole story.

Different distros (and different Unix-like OSs) install things (such as header files) in different places. Even within a distro, installing a package from source (rather than through the package-manager) may result in it being placed in a different directory. On Ubuntu the convention is that /usr/include holds headers installed through the package-manager, and /usr/local/include holds headers for libraries installed from source.

Of course, Windows isn't a Unix-like, and things have to be handled differently there.

The configure script should figure out that stuff automatically, the user shouldn't be given a pile of errors to manually fix. They shouldn't have to make any source-code changes just to get things to build.

Robustly portable scripts to find packages, can be surprisingly involved. [0] This is why CMake encourages us to write self-contained Find... scripts that be reused by other codebases.

> Both of these things can be perfectly done with plain makefiles

Doing so manually with raw Makefiles, isn't scalable. This is why systems like Autotools exist. The configure script handles this sort of machine-specific detail.

Large programs may have dozens of dependencies. It may be a sizeable task for the user to fix them all.

> The alternative is to have your build system to search around your disk for possible versions of the required dependency. This is a completely untoward behavior and in very bad taste.

C and C++ are not like Java. There isn't a single idealised system that you can target. You are exposed to platform-specifics, and it's on you to cope with them. This is true both at the source level (good code doesn't rely on sizeof(long) giving any particular value), and at the build level.

[0] https://github.com/Kitware/CMake/blob/master/Modules/FindIma...

Thanks for the detailed answer. I see where you are coming from, but it seems to me that some of your assumptions guaranteeing the need of autools or cmake in unix systems are a bit outdated.

> Different distros (and different Unix-like OSs) install things (such as header files) in different places.

This doesn't matter. If the distro is somewhat decent, then once a library is installed they it is found by the compiler. This is certainly true of ubuntu, slackware, voidlinux, openbsd and macos. This means that if you want to use a dependency foo.h, then you can #include <foo.h> inside your code and add -lfoo to your compilation line (inside the makefile) and everything works smoothly, regardless of where the actual headers and libraries are installed. This is true for the clang, gcc and icc compilers. If libfoo is not installed, the compilation will fail, as it should do. Everything else is unnecessary complexity. The actual directories where the files are installed are irrelevant; they are supposed to be found by the compiler (for example, through the CPATH and LIBRARY_PATH environment variables, or by the default compiler specs installed in the distro).

This is perfectly scalable. If you need twelve libraries, foo1 to foo12, then of course you need to add -lfoo{1..12} once in your makefile, typically inside your LDFLAGS variable.

Cmake is the work of the devil, it does not solve any problem and it needs to die, just as autotools.

a) this doesn't apply to all distros in practice b) this definitely doesn't apply to non-unix systems c) This ignores the fact that often a user may want/need to control which version of a library they link to, including one which isn't installed in the system (I personally frequently build stuff with intermediate dependencies which are not available in my distro: installing these on the system level is madness especially when you have multiple projects, so I make sure everything is isolated in a project directory). Being able to control where libraries are picked up from is very important in these cases (though of course cmake does have an impressive ability to get it wrong when trying to autodetect).

You may wish for a situation where libraries can just be specified easily and consistently (and many languages have a package manager which does just that), but the reality for C++ is incredibly messy (because there's not one package manager but several, most lack a way to distinguish between project and system package or an easy way to add your own, and some platforms still don't have a package manager at all) and a build system which does not deal with that messy reality does not have a hope of adoption.

Cmake sucks. autotools even more so. But denying that the problem they're trying to solve exists does not help.

I agree 100% with almost everything you say.

Regarding "a)", can you point me to a distro where after installing a library "libfoo" from the standard packaging system, the C compiler fails to find "foo.h" and the linker fails to work with "-lfoo" ? I've never found such bizarre behavior.

Regarding "b)", granted. Portability to microsoft compilers is a bitch, and may as well require cmake.

Regarding "c)", this is an impossible problem. If you install a library on ~/my/particular/path/to/libfoo, and you want to compile against this, then of course you will have to specify that particular path manually inside the compilation system. This is possible with cmake, and there's even a beautiful user interface for that. It is just as easy with plain makefiles: you just add the particular path to the C_INCLUDE_PATH and LIBRARY_PATH variables (they work exactly as PATH). No need to mess with -I nor -L. Notice that here you are not using any of the cmake "detection" capabilities, you are just specifying the path manually. This is correct behavior, of course you would not like cmake to spy on its own inside your home dir to see if it can find a certain dependency.

> You may wish for a situation where libraries can just be specified easily and consistently

Yes I do. And I'm very happy that this is the case.

> but the reality for C++ is incredibly messy

thanks to autotools, cmake, and other tools written by people who do not understand the concept of PATH. The reality is easy, not messy, it's just people who make it messy intentionally; more out of ignorance, not of malice.

Look for example at the imagemagick "detection" rules shown by the OP as a pinnacle of useful and complicated thing that cmake does. It is completely unnecessary bullshit! If imagemagick is installed, then these executables are already on your PATH, and they are found automatically by the shell (that's exactly its job!). If imagemagick is not installed, or if it is installed on a secret directory, then there's no reasonable hope for cmake to "detect" it (other than scanning the whole disk, which I hope it does not try to do...); and if you want to use this secret installation of imagemagick you will have to specify its path manually somewhere in the compilation system. For example, by setting a PATH variable inside your code.

Everything that you say is right, and it reinforces my conviction that "detecting" installed libraries never needs to be done. Yet you have hordes of idiots writing detection rules for cmake. What a time to be alive.

Another example: FindBLAS. [0] There are various different compatible BLAS libraries, and the elaborate CMake script accommodates this.

To return to a point I made earlier, part of the value of having a configure step before the build step, is to handle all the platform-specific stuff early, failing early and with good error-messages if something is missing. I've seen builds fail several minutes in, it's very annoying, and the configure step should make all the checks necessary to prevent this happening. If the configure step succeeds, I should be able to rely on make succeeding.

You make a good case that this step should really be very simple, and I agree, especially considering pkg-config exists, [1] but here we are. (True to form, CMake is able to leverage pkg-config, but makes it painful and confusing. [2] You have to be a master of CMake to figure it out, and the correct answer depends on the CMake version, despite that both pkg-config and CMake were first released in the year 2000.)

Also, optional dependencies can be handled properly. I don't see how you could easily handle this with pure Make.

[0] https://github.com/Kitware/CMake/blob/master/Modules/FindBLA...

[1] https://en.wikipedia.org/wiki/Pkg-config

[2] https://stackoverflow.com/q/29191855/

If the syntax is the primary problem, there is Meson (reduced Python) and XMake (Lua).

I wonder why CMake cannot embed an alternative language? You could have CMakeLists.lua files modify the same targets and properties using Lua.

Those are entirely different build systems, and they're obscure and immature.

CMake scripting is bad. Fragmentation of build-systems is probably even worse. Like I said, it still makes sense to use CMake for multi-platform code.

If the CMake folks made more of an effort with documentation, many of my complaints would go away. It's not necessary to burn down CMake and start afresh. The awful language wouldn't matter so much if there was clear and thorough documentation with plenty of complete examples, and in particular, definitive guidance and reusable boilerplate for the most common jobs (e.g. package discovery).

> I wonder why CMake cannot embed an alternative language?

The last things CMake needs are even more complexity and even less consistency.

I personally find it surprising that so many people are still stuck on old, outdated build systems like CMake. Modern alternatives like Bazel (https://bazel.build/) are light years ahead.
Right now, CMake is being actively integrated into QtCreator and Visual Studio where they previously had their own bespoke defaults. It might be shit but it's also becoming more and more "standard" as one DSL you need for cross-platform builds, instead of fifteen xkcd-927 style.
The claim that CMake is the least worst C++ build system that we have is pure BS.

In my last 3 professional C++ projects I’ve actually used Rust’s cargo tool as my build tool for C++ code. Infinitely better than CMake, plus you use Rust as your scripting language for your build system, which has great libraries for it like the cc crate, and pretty much a crate for whatever it is you want to build (eg ispc? no problem).

Fluffy, could I ask you to give a few more details about how you set this up? Sounds interesting!
Does it support a variety of operating systems and IDEs, the way CMake does? Can it handle package-detection across all those platforms?

CMake has a terrible scripting language, but its backends are very good. Being able to generate Unix makefiles when working from GNU/Linux, and then generate Visual Studio solution files when working on Windows, is pretty great.

I've given up on these sorts of build systems. If I ever had to screw with a C/C++ codebase again, I would just write my entire toolchain in C#. I know for a fact that modern C# 8.0+ will not bite me in the ass with inconsistent ideologies. I also have extreme confidence that I could add in complex build concerns such as communication with other systems for obtaining dependencies or logging important information.
> CMake allows us to handle almost all those things right except one - it doesn’t have return values

One of my former bosses once quipped "CMake is confusing until you realize it's just a shitty version of BASIC that only has global variables"

Makefiles combine the expressivity of Bash with the clarity of Bash.
Is there a dialect of BASIC that has scoping/namespaces or even locals?
Visual Basic was ok from what I remember.
Technically yes, also VB.net . Reminds me that old joke that any language can run on .Net runtime (a.k.a. CLR) as long as that language is C#. Somehow VB.net is kind of C# with VB syntax and some corners cut that they could not fit.
So glad that new languages don't need build systems.

go build .

I'm sure you can find people ranting about how their "xxx_windows.go" files got silently ignored by go build. Just like every part of Go, everything ugly is hidden and "just works" until it can't, and major WTF moments strikes after that.