But the scripts don't work without the cpp - you can't really excuse the build system without excusing the cpp file. All of the actual logic for the build system is in the cpp file, and at least on windows it fails at the first hurdle - it blindly assumes cl.exe is in your path which is not as common on windows as it is on linux for example.
It’s perfectly legitimate to say “run the script from a shell where the environment is set up”. In fact, it’s preferable, and you should only attempt to be “smart” and search for that type of thing if you really need it — why use any search logic at all, if it can be avoided. vcvars and its modern incarnations exist to facilitate this.
I had already skimmed the cpp (at least the `build` functions). It’s not necessary for what they’re doing. It could be easily replaced with a shell script. I know because I’ve done something very similar, supporting I think even more build variants and compilers. This is mostly copied from a real script, except I mocked up `build` somewhat, since the real one is slightly more complicated.
build() (mkdir -p "$1" && cd "$1" && shift && “$@“ ; )
...
declare -a gcc=(-std=c89 -g -D_GNU_SOURCE -fpie -pie -fno-strict-aliasing -Wall -Wextra -Wformat=2 -Wstrict-aliasing=2 -Werror)
declare -a clang=(-std=c89 -g -D_GNU_SOURCE -fpie -pie -fno-strict-aliasing -Weverything -Wno-unused-macros -Wno-used-but-marked-unused -Werror)
declare -a debug=(-O1 -D_FORTIFY_SOURCE=2 -fno-omit-frame-pointer -fno-optimize-sibling-calls)
declare -a asan=("-fsanitize=address,undefined,leak")
declare -a msan=(-fsanitize=memory)
declare -a isan=(-fsanitize=integer)
...
build "$build/x86_64-linux-gnu-gcc-addrsan-debug" gcc "${gcc[@]}" "${debug[@]}" "${asan[@]}" -o example "$src/example.c"
build "$build/x86_64-linux-gnu-clang-debug" clang "${clang[@]}" "${debug[@]}" -o example "$src/example.c"
build "$build/x86_64-linux-gnu-clang-addrsan-debug" clang "${clang[@]}" "${debug[@]}" "${asan[@]}" -o example "$src/example.c"
build "$build/x86_64-linux-gnu-clang-memsan-debug" clang "${clang[@]}" "${debug[@]}" "${msan[@]}" -o example "$src/example.c"
build "$build/x86_64-linux-gnu-clang-intsan-debug" clang "${clang[@]}" "${debug[@]}" "${isan[@]}" -o example "$src/example.c"
...
Very easy to write a powershell version for Windows.
If you know the compiler flags you want, nothing beats literally just writing them all down in a script, when it comes to debugging issues.
> you should only attempt to be “smart” and search for that type of thing if you really need it — why use any search logic at all, if it can be avoided. vcvars and its modern incarnations exist to facilitate this.
But that's the standard for how to do things on Windows. That's how it works. Sure you might not like it, but personally I don't like the implicit assumption that everything is configured and "there" that your scripts and the OPs scripts assume.
All of a sudden you have support for all major platforms, the workflow is the same (meaning you don't have platform dependent scripts like build_win and build_linux), it works out of the box with all major IDEs/editors, supportd optimised and stripped builds easily, and abstracts that away across platforms (-O2 Vs /O2, /Z7 instead of objcopy +strip). These are problems that have been solved for 30 years, and makeshift bash/batch scripts only make it harder for people to understand what's going on.
It’s fine if you want searching, but I disagree that it’s implicitly wrong not to do it because searching is supposedly standard and how everyone else is doing it. Never mind that there exists plenty of software (with far more specific requirements than “give me an object file/dll/executable, compiler version/ABI/options be damned”) that doesn’t use such an approach. For example, try to tell CMake how to create a freestanding executable (one that doesn’t link to the system C library). You end up fighting against the abstractions inherent in its design. Which is to say, depending on what you want to do, it might not be a good fit, and you may be better off just writing out your compiler options yourself.
Here’s part of the powershell version of a build script I have used, where I had fewer build variants, so I didn’t even bother to make a `build` function, I just copy paste the `out=... ; if { ... }` block and change the value of `$out` and which compiler and options are called. Note that you can accomplish this with far fewer variables if you don’t want quite as many build variants as this script supports — it was written as an illustration of how flexible you can make things, rather than how simple things can be, so it supports clang, clang-cl, cl, zig cc, with variants for asan, freestanding, for architectures x86, x64, aarch64:
If you have an issue, you can easily change compiler options. It will always work and won’t suddenly break if you install a second set of compilers for another project, whereas CMake will have to choose which one and might pick wrong. Likewise, there is no potential for an upgrade to CMake to decide to use different compiler options that interfere with the freestanding stuff. You lose CMake’s abstractions, but like I said, that may be a good thing, depending on what your requirements are, assuming you have detailed knowledge of the compiler (which I’d rather accumulate than detailed knowledge of CMake, and I would know, I have both).
> For example, try to tell CMake how to create a freestanding executable (one that doesn’t link to the system C library). You end up fighting against the abstractions inherent in its design.
What's wrong with
target_link_options(foo PRIVATE -static-libgcc -static-libstdc++)
I don't understand how that particular powrshell block is more flexible than cmake from what I can see it does less (it's windows specific - mine works on Linux, Mac, windows, and also works with cross compilation toolchains out of the box). If I want to add specific warnings I can use target_compile_options to get exactly the same behaviour.
> won’t suddenly break if you install a second set of compilers for another project,
I'm genuinely curious - have you used cmake? Cmake has sensible defaults but is completely customizable. Cmake uses platform standards - if you overwrite the CC variable it will use that toolchain instead. Cmake has its warts, for sure, but lack of flexibility is definitely not one of them.
I have extensively used CMake since years before it supported the target_ options, and years afterwards. So I was using it before, during, and after the breath of fresh air that was the shift to what is now called “modern CMake”, i.e., bjam/b2 style (define targets with private/public/interface dependencies/options). As you can guess, I’ve also used bjam/b2 extensively. I’ve also used meson to a more limited extent. And when I say “used”, I mean where I’ve written many build files from scratch myself, in a professional capacity.
I’m always the “build system expert” on my team because of how much experience I have. If working on a team where people know the compilers, and where build times are kept reasonable (so not using lots of templates in C++), then on a practical level I recommend not using a build system at all, and just writing a super simple shell script (unity builds are easier there too). I still recommend CMake sometimes though — especially if the main requirement is just “gimme some machine code, who cares how, and who cares about using ASAN, etc.”, and the second most important requirement is to ship the code to the open source community.
BTW, I mean freestanding as in “does not link to any C library”, rather than “statically links to the C library”.
declare -a free=(-std=gnu99 -DEXAMPLE_FREESTANDING -nostartfiles -nostdlib -nodefaultlibs -ffreestanding -fno-stack-protector)
> then on a practical level I recommend not using a build system at all, and just writing a super simple shell script (unity builds are easier there too).
But the minute you introduce someone who is familiar with windows or prefers an IDE workflow this falls apart. You are also limited to the platforms, compilers and IDEs that you write the script to support. That's not flexible, that's inflexible. Also, cmake has supported unity builds out of the box for a few years now [0] and before that cotire existed.
> I mean freestanding as in “does not link to any C library”, rather than “statically links to the C library”.
Ok, great so just use target_compile_optiond in the exact same way as you've just listed the arguments? I don't see how cmake stops you from doing that by architectural decisions at all.
Writing a new “call the compiler like so” line is pretty easy — pretty flexible, I’d say. Let’s say a new compiler came along tomorrow, where all the options have plus signs instead of minus signs (“+O +DPREPROCESSOR_DEFINITION”), I’d have an easier time adding a single line to a script than I would writing a toolchain file for CMake, or more likely, having to look at the CMake source code. That is a not unreasonable, but still quite pathological example. But it’s a window into the annoying world of manipulating compiler commands via the indirect mechanism of CMake. If you have very precise requirements, especially regarding order of arguments on the command line (a common issue with cl but to a lesser extent other compilers also), then using CMake can be like performing surgery with chopsticks. If you have simpler needs, it can be like eating sticky food with chopsticks — i.e., a convenient way to keep your hands clean. It seems like I just disagree with how steep the slippery slope is between those two points.
As for editors and whatnot, I’ve worked on teams where people used Visual Studio, vim, CLion, VS Code, via these scripts. Most with full IDE integration, some without (some people don’t use it). The easiest pathway is to generate compile_commands.json — ironically, this is already something extremely close to the script, so it’s trivial to automatically generate from the script.
I was just saying as a postscript that I meant something different by freestanding. I agree that it’s totally orthogonal to your argument and mine.
> If you have very precise requirements, especially regarding order of arguments on the command line
Then write those arguments in that order with target_compile_options?
> The easiest pathway is to generate compile_commands.json — ironically, this is already something extremely close to the script, so it’s trivial to automatically generate from the script
You keep saying it's trivial to extend it to do X and to do Y, and yet it's even more trivial with cmake - it already does it. At a certain point you've just poorly reimplemented something else
I've never had to add support for a brand new c++ compiler to cmake personally, so I'll have to trust that yes it's easier to modify your she'll svript to support that, but given that 99% of my build systems work is generating IDE files and building with MSVC, GCC and clang (even across servers, mobile devices and consoles), ill continue to recommend the system that works for that rather than a hypothetical new c++ compiler that will likely require more time spent on conforming the code than modifying a cmake file.
I’ve repeatedly wasted hours on issues intrinsic to CMake (not respecting options, overriding my options by passing contradictory ones in a way that’s beyond my control, etc., but also you guessed it, bugs in CMake, or issues with its awful and poorly thought out language). I’ve never had issues intrinsic to a script that just calls gcc or what have you. Any issues I’ve had with builds using scripts are to do with learning how a specific compiler works, something I have to do anyway, and therefore it’s never wasted. Sometimes but rarely enough, I even use CMake as a starting point (especially if I can’t access my previous scripts right at that moment), but I just extract the resulting commands, write the script, and delete the CMakeLists.txt.
I have ported scripts to use `zig cc` (though that uses a gcc/clang-like set of options by design, so easy in CMake also), and `tcc` (again gcc/clang-like, but only supporting a small subset of options, so with a script, I can just remove those arguments and the “port” is done — suppressing options CMake wants to give is harder).
As for poorly re-implementing something CMake already does, I disagree when CMake doesn’t do all that much (CMake’s language is not great at doing much beyond specifying basic target options). Writing compile_commands.json for a gcc toolchain is a one-liner callout to python, just `print(json.dumps({“directory”:sys.argv[1], “arguments”: argv[2:], “file”: argv[-1]}))` given the same arguments as that `build` function from earlier. This tiny extra up-front cost reaps rewards for every ship cycle where you don’t have to fight with the tool, or even learn it in the first place (think about issues like passing -D options to CMake, then reconfiguring the build in the same build dir but with different options — does this always work or do what you expect? in my experience, absolutely not). I won’t complain about the general quality of typical CMakeLists.txt files because the general quality of scripts isn’t great either! But it’s nice to limit the number of poorly taught and therefore poorly written scripting languages involved in the build, and shell is far more generally applicable, so it’s a worthwhile investment compared to CMake/meson/waf/b2/xmake/bazel/whatever else is coming down the pipe. (If you’re a massive organization, we’re talking collaboration across the globe in ways that exceed normal methods of human collaboration, and that result in monumental build times and complexity, then invest in a fancy tool like bazel, it’ll probably be worth it for you).
I’m perfectly happy for people to continue to use CMake, so I’m not saying other opinions are wrong, and I wouldn’t immediately proselytize a team to switch away if I joined it — one of the reasons I don’t tend to use it is because focusing on build tools is a huge time sink, but it’s also a time sink to start a debate about transitioning to a script. But I’m happy not having to deal with it in many situations.