I don't think that whether a program is small or large is the issue. The problem here is that most build systems suck.
I work on large-ish scale Rust software (~250kLOC), we ship to all major OSes and many minor ones: android, macos, ios, windows, linux, freebsd, openbsd, ... we build binaries for x86 (32 and 64 bit), arm6, arm7, arm8, arm64, ppc, ppc64le, sparc64, s390x, wasm, ... and many platforms (e.g. glibc and musl on linux).
We did pretty much all of this with a wrapper over Rust's cargo build system called "cross", and to build for a target, with just had to write `cross build --target=...` instead of `cargo build`. If you want to run your tests for a target, you can actually write `cross test --target` and your tests will run.
For a long time, that was it. Pretty much zero setup, cross compile ready-to-publish binaries to any target, run the tests of your program on that target, all of it by just writing cross instead of cargo. It just worked.
The way cross works is quite simple. It comes with a bunch of maintained Docker images, containing cross-compilation toolchains for C, C++, ..., right versions of kernel headers, glibc/musl, some utilities, etc. These also come with qemu-user or qemu-system, depending on the target. So cross would use the Rust compiler and the C toolchains and appropriate libraries to cross-compile on your HOST, which is quite fast, and when running the tests, it would often just run them under qemu-user, which is quite fast as well. For Windows targets it uses wine, for Android targets it install the SDK, NDK, emulators, for iOS you can use the iOS simulator that come with Xcode, etc.
Cross is by far, the damn best build tool I've ever used. Zero configuration, and it not only worked, but it did a lot. Want to target sparc64? No problem: cross test --target=sparc64-unknown-linux-gnu compiles your project runs your test, and you are good to go.
The reason we stopped using it wasn't that our project got big. It was that, at some point, the build tool wasn't enough anymore.
We added more dependencies, and needed to modify the Docker container slightly. cross lets you reuse their containers to build your own adding whatever you need, so we did that for a while, and we still do.
The MacOS support wasn't really there, so we were running macos and cross natively. There is another Rust build tool for macos, that is like cross, but supports iOS, so you could run `dinghy test --device=...` and it would cross-compile to the device, copy the tests to the device, and run your tests on an aarch64 device. That meant having to ssh into the right machine, install the tool in your account, and do all that, just to see if a test fails.
At some point we wanted to start supporting other targets, like FreeBSD and OpenBSD. Both operating system are, in my experience, very hard to cross-compile to, providing no cross-compilation toolchains for Linux at least. So with cross we were compiling to FreeBSD under qemu-system which was super slow. When we started supporting OpenBSD, we ended up spawning native FreeBSD and OpenBSD VMs, and compiling directly from those hosts.
So how are we doing all of this right now ? Well we initially forked cross, which is written in Rust, and hacked some specific modifications in, but at this point we just wrote our own crappy duct-tapish python script, that calls cross when it can, and does something else when it cannot.
This is where the barrier of entry for new developers appears. We aren't using cargo, cross, or anything else that they are familiar with. We are using an undocumented, super hacked, unfamiliar solution, that calls other build systems. Most of the time, `./b.py build` or `./b.py test --target=xyz` is all they need to know, but if they need to modify our build system, the curve is steep, because they now need to learn about all the horrible problems it solves and the super hacky ways in which it solves them.
So why do Firefox, Chrome and other big projects have their own build systems? Being big has nothing to do with this. These projects run on all versions of all OSes on all hardware that these OSes support with all possible user-system configurations running the tests that must be run for each of these configuration and generating and publishing documentation, packages, and artifacts for all sorts of documentation and package formats to all sort of domains while preventing hundreds of developers trying to break everything with their changes from doing so.
Imagine if we were to solve this problem writing Makefiles, CMake, SCons, or any other crappy DSL of any build system in existence. I'd rather suffer for all eternity in hell.