But abstractions are a good thing! They let you separate a system into layers, so that things programmed at a higher layer don't need to know all the details of the lower layers. It's the way we separate interface from implementation.
Let me give an example from Bazel. Suppose you are compiling some C or C++, and you need to define a preprocessor symbol. There are two ways of doing this, "defines" and "local_defines":
cc_library(
name = "my_lib",
srcs = ["my_lib.cc"],
hdrs = ["my_lib.h"],
# This will pass -DFOO when compiling this library
# *or* any library that depends on it.
defines = ["FOO"],
)
cc_library(
name = "my_lib",
srcs = ["my_lib.cc"],
hdrs = ["my_lib.h"],
# This will pass -DFOO when compiling this library
# only. Libraries that depend on us will *not* get
# the symbol.
local_defines = ["FOO"],
)
You can use "defines" when you have #ifdef statements in headers and "local_defines" when you only test the symbols in your .cc files.I did not have to think about how defines will propagate to the libraries that depend on me, I just specified what I need and the build system handles the rest. I did not have to define my own CXXFLAGS variable and handle concatenating all of the right things together. The lower layer takes care of that.
What Bazel lets you do is create abstraction boundaries within the build system. People can write rules that define a self-contained set of build logic, then other people can use those abstractions without knowing all the details of how they are implemented.
Bazel is not perfect, and I have found myself banging my head against the wall many times. But overall the ability to define abstractions in a build system is, I think, one of the biggest things it gets right.
Disclosure: I work for Google, but not on Bazel.