But... if you climb that cliff, there are many rewards at the top. Like the article notes, you can reuse intermediate build artifacts between developers (because it's not possible for those artifacts to accidentally depend on developer-specific state), you can automatically parallelize (because the build system can tell declaratively which tasks do no depend on each other) and you can ask it interesting queries about the dependency graph. That sounds an awful lot like the runtime safety, performance, and static analysis static types give you.
The challenge is that for small programs, dynamic types are just easier to get going with. And every build script starts pretty small. So you have a plethora of simple, hackable build systems like make that work fine at first but then get more and more painful as the program grows. With software, there is at least some customer-visible pain that can enable you to justify a rewrite in a statically-typed language. But build infrastructure is always a hidden cost so it's really hard to get the resources to migrate to a different build language.
Maybe the lesson here is to bite the bullet and start with a hermetic build system at first.
cc_binary(
name = "app",
srcs = glob([ "*.hpp", "*.cpp" ]),
)
That's more concise than most build systems AND it's cross-platform.A lot of work goes into those 4 lines. This is evidenced by the simple question of, how do you set up a custom compiler for that code properly? The answer being a couple hundred lines of starlark code, for what in other build systems is effectively `CC=clang` (to be fair bazel allows this but does not support or encourage it, especially for anything besides toy builds).
I like Bazel (a lot!) but you are oversimplifying it here.
The issue is that declarative build systems ALWAYS need an escape hatch for the exceptions--and the exceptions grow over time.
We've been here before. It was called Make. And then it was called Ant. And then Maven. And Google still built their own system. And they will build another again.
Nobody ever learns the lesson that a build system is a program--period. Sorry to burst your bubble: there will be circular dependencies--you will have to deal with it. There will be two different C compilers on two different architectures. You will have to deal with it. Libraries will be in the wrong place--you will have to deal with it. These libraries come from the system but these libraries come from an artifact--you will have to deal with it.
The only way to deal with it is to have a genuine programming language underneath.
$ cat Makefile
SRCS = foo.cpp bar.cpp baz.cpp
OBJS = $(SRCS:.cpp=.o)
PROG = app
EXT =
.PHONEY = all clean
all : $(PROG)
LDLIBS = $(OBJS)
$(PROG) : $(PROG:$(EXT)=.cc) $(OBJS)
clean :
rm -f $(PROG) $(OBJS)
$ make SRCS="`echo *.cpp`" clean all
rm -f app a.o b.o c.o
c++ -O2 -pipe -c a.cpp -o a.o
c++ -O2 -pipe -c b.cpp -o b.o
c++ -O2 -pipe -c c.cpp -o c.o
c++ -O2 -pipe app.cc a.o b.o c.o -o app
$Note that Bazel builds still depend on the ambient system environment, like the compiler toolchain and system libraries, and it's possible to poison a shared build cache.
Shared caches are typically used with remote execution and CI servers, where the build environment is fully controlled.
The only way to poison the cache then is to break things at an OS level, which is highly unlikely.
However, there are at least two cases where you do not want real hermetic builds:
- in the case of open source software that needs to be packaged for distributions, vendoring all the dependencies is bad. As the name already says, distributions remix software components and thus might want to compile your software against a different version of a library than the particular version you pick. - for libraries, consumers use your library just as a component and might want to combine it with a specific version of other libraries, so vendoring also sucks.
Unfortunately, many declarative build systems make it hard to separate the "building a component" part from the "composing a product" part. I could find no way in Bazel to depend on system software (using for example pkgconfig).
In my opinion, build systems for open source software (as I mentioned, for companies Bazel makes sense since you often want to vendor all your deps anyway) need to have three aspects:
- build rules for just for this component - dependencies on other components/libraries - a method to lock down components/libraries to a specific version, creating an offical "product" with an official supported combination of all dependencies.
This still makes it possible for distributions to create their own "product", without having to rewrite most parts of the build system. At the same time, developers can work with the official library versions (for compat, maybe you should even have multiple "sets" of dependencies), so getting started is easy and you don't need to install lots of dependencies first before you can hack on the project.
Often, build systems do things that are nice for onboarding (like "build systems" that pull git repos of dependencies during build, in order to have a single command to build the product from scratch after checkout). But those things directly conflict with the needs of distributions. We need to separate the "instructions to build, supplying all deps externally explictly" (in the context of building a package for a distribution) from the "just build it from the command line on a dev machine" (here, dependencies should be implictly fetched automatically, to give a nice user experience).
[1] https://github.com/bazelbuild/bazel-gazelle/blob/master/inte...
Build systems have the annoying property that they are ultimately highly declarative, but they need some pretty extreme scope to handle edge cases. Any C/C++ project is going to boil down to "compile these N source files, and link them into a library/executable", but the necessary build flags is going to come from several locations, there are multiple variations of build flags and compilers that need to be used at the same time, and there is going to be project-custom code to generate source files that involves potentially building the tools to generate the files as part of the build system. And mixed-language projects add more layers of pain.
We had a particularly "fun" instance of this sort of Makefile weirdness back when we used Make for Rust... https://github.com/rust-lang/rust/blob/efec34a95a76212b2324d...
(Discussed on HN: https://news.ycombinator.com/item?id=13655081)
That might make the installed size large, but if it's isolated to Bazel, managed by Bazel, updated by Bazel, etc, then why does it matter whether it's a JVM, a Python binary, a JS VM, or anything else?
Even if you write custom rules, you use the Python-like Skylark language and never interact with Java code.
[1]: https://github.com/bazelbuild/bazel/releases (the single-binary distribution is bazel-1.1.0-linux-x86_64, it's only 40MB).
And in the case you don't want to, it is hardly any different from shipping a bunch of .so/.dll with the application.
I suppose I see the benefit of this setup if you keep many libraries and want to pin to the latest instead of, an explicit lib version. If you're in a multi-repo system you're probably not doing this but I can see how Bazel could actually make this possible.
Can someone shed some light on the CI story? Can Bazel do code deployment as well? Is that a smart thing to do with Bazel?
A pure Go codebase? Not worth it.
Personally, I use Bazel even for small projects as soon as they involve things like generated code or gRPC.
Yes, there are multiple rulesets for deployments, like rules_k8s[1] and rules_docker[2]. Of course, you can easily build your own custom deployment pipeline.