1. Rule with multiple outputs are problematic. Stamp-files work as a hack, but the build ends up harder to maintain as developers need to figure out which stamp controls what files
2. There's no good solution to handle changes in the makefile. Particularly, if a make target disappears, there's no way to delete the output file. If another build step later on grabs the supposed-to-no-longer-exist file via a glob pattern, build failures or run-time fun can result. Workaround: attempt a clean rebuild whenever the build does something weird. This is especially problematic when switching between development branches.
3. Recursive makefiles suck because you can't have fine-grained dependencies between parts of the sub-projects. Non-recursive makefiles suck because it's just textual includes: there's no scoping, so name collisions between different sub-projects cause trouble. And all paths have to be relative to the top-level makefile, so there's no way to write a makefile that can be included in arbitrary other projects.
4. Some features have weird semantics that makes them unusable in practice; e.g. the "target: export A=B" syntax looks like it's setting an envvar locally for a single target, but actually also sets it for other targets "invoked by" the target. Of course, which dependencies of "target" are "invoked by" it ends up being non-deterministic in a parallel build.
Fun fact: even "target: private export A=B" doesn't work reliably; the "private" modifier in GNU make only fixes this problem for make variables, but not for environment variables.
So is there a build tool that fixes these problems, instead of just replacing the parts of make that actually work well?