RMake: A Modern GNU Make Alternative written in Rust
I've often thought that make is a solid tool, but it could really benefit from modern features like package management and file watching capabilities.
After considering this for a while, I decided to stop just thinking about it and actually build something. The result is a Rust implementation of GNU make that aims to be 100% compatible with GNU make 3.81 (the version shipped in macOS that many developers target) while adding features that address some long-standing pain points.
The project is still in its early stages, but I'm excited about the progress so far and believe it could become a practical tool for real-world development in the near future. While full compatibility is the first priority, I'm already designing some extensions that could make C/C++ development significantly more pleasant.
Structure
Right now everything is still sync, but in the long run I'd like to utilize an async runtime like Tokio here since it seems as though make mostly waits on other processes/IO. Regarding the parser, I'm currently experimenting with pest which can create parsers from PEG grammars, although I do quite enjoy writing my own parsers.
Why Rust?
Mainly because I was searching for a small project to prepare for a job interview, but generally I think for a project like this there are only a couple of viable languages:
C
Obviously C would have been a fine choice as well, especially with something like libevent. Though I kinda feel that while in doubt, Rust is a better choice for system software if the underlying hardware is somewhat modern/performant.
C++
Not a fan of C++ to be honest. For the most part, I think that Rust does everything C++ does, just better. Although I have to say GameDev is a scene where C++ seems like an OK choice.
Go
Go seems like a great language for a project like this, and I might just go for it just to have an excuse to finally do a deep dive into Go.
BSD Make
I actually prefer the BSD Make syntax for the most part, so in the long run I'd like to support both syntaxes or provide a switch in the makefile so that one can choose without having to use a different program (mainly since it seems like the difference is only superficial if one ignores things like the embedded Guile).
Future improvements
Now I'd like to write about 2 features that particularly excite me and that I'll hopefully come around to implementing.
Watch mode
I'd love to implement a "watch mode" feature activated by a command-line flag. In this mode, instead of running once and exiting as make normally does, the program would:
- Build the requested targets
- Continue running in the background
- Monitor all input files for changes
- Automatically trigger a new build whenever any dependency is modified
This would create a seamless development experience, especially for projects where you frequently make small changes and want to see immediate results.
Package management
This is gonna be a tricky one. Right now I'm thinking of having a special format for specifying package dependencies, for example like this:
PKG(ncurses): PKG(ncurses@6.5)
prog: main.o PKG(ncurses)
$(CC) -o $@ $<
The core idea is simple:
- Use the PKG(ncurses@6.5) syntax for depending on a particular version
- By using the familiar dependency syntax we can specify which particular version of a dependency we'd like to use
- This could be moved to a separate Makefile that's included from the main one, giving us a separate lockfile
- Your build targets can directly depend on packages using the
PKG()
syntax
For implementation, I'm considering these key requirements:
-
Cross-platform compatibility - Must work across different operating systems
-
Source-first approach - Support compiling from source, with binaries as an optimization
-
Standardized package format - An archive containing:
- Standard directories (
/lib
,/include
,/bin
) - Metadata file (likely JSON or TOML)
- Architecture information in both filename and metadata
- Hash of configuration options used
- Standard directories (
-
Flexible linking options - Easy switching between dynamic and static linking
The workflow would likely involve cloning repositories and running specialized make commands to compile everything into binary packages. This approach would create consistency between how we handle binary dependencies and how we build packages from source.
Conclusions
There is still a lot to be done, however I'm quite excited about it and especially the Package management feature is something that's sorely lacking from C in my opinion, though I should probably give vcpkg another try. My first impressions weren't that great though.
Unlike build systems like CMake or Bazel that try to replace make entirely, RMake aims to maintain compatibility while incrementally adding modern features. This approach allows developers to migrate gradually and maintain compatibility with existing build ecosystems.
My immediate focus is on achieving full compatibility with GNU make 3.81, with watch mode and package management features to follow shortly after.
If you're interested in following the development or want to contribute, check out the project on GitHub.