Stirmake is a rewrite of the venerable make. It is intended to make build
systems easy to understand and maintain for mere mortals. At the same time, it
is intended to once and for all eliminate the need for frequent make clean
.
To understand the motivation behind stirmake, one needs to first understand why recursive make is harmful[1]. Then one needs to attempt to create a non-recursive build system using make[2], finding it's very hard to do so, and the result is an almost unmaintainable mess. One also could explore the alternatives for make such as Makefile autogenerators (hint: a chain is as strong as its weakest link, and a weak, not necessarily the weakest, link is make), and standalone alternatives. The standalone alternatives include SCons, Rake and Shake of which SCons and Rake are implemented in a slow interpreted language, and Shake uses a strange Haskell-based input syntax.
- Fast C implementation, with performance comparable to make
- Intuitive syntax, similar to make
- Support for parallelism
- Support for including sub-Stirfiles in subdirectories
- Support for including Stirfiles past project boundaries, controlling what is visible to subprojects
- Executes shell commands in subdirectories, like recursive make and unlike inclusive whole-project make
- Proper data types; finally, filenames can have spaces
- Programmablity with a custom language
- Prevention of running two simultaneous instances for same project hierarchy
- TODO: LuaJIT integration
- Multiple targets per rules
- Dependency on a whole directory hierarchy, using its latest mtime
- Compatibility with
gcc -M
format dependency files - Conditional compilaton
- Build command database, with dependency on build command
- Automatically deduced cleaning rules
- Fast bytecode based variable expansion
- Many sanity checks; some fatal errors, some helpful suggestions
- Support for invoking build tool in any directory, much like you can invoke git in any directory and it automatically detects where the
.git
top-level repository is located - Full GNU make jobserver integration, allowing parallel sub-makes
- Modification of dependencies on the fly, so that the built system can affect the build system
Stirmake is built in the following way using GNU make to bootstrap it:
git submodule init
git submodule update
cd stirc
make
The compiled stirmake executable is fully self-contained. No dynamic libraries are required apart from the ones that come with the operating system.
Note there is no make install. One needs to manually copy the stirmake binary to some directory and create the symlinks:
cd stirc
sudo ./install.sh /usr/local
sudo mandb
It's also possible to install without root permissions locally:
cd stirc
./install.sh
...which installs it to ~/.local
. If the directory ~/.local
does not exist,
it needs to be created with mkdir
. Note that in some cases, when installing
to ~/.local
, the ~/.local/bin
might not exist (it is automatically created
then). If the directory ~/.local/bin
needed to be created, it might be
necessary to log out of the Linux system and log in again. In graphical logins,
closing the terminal may not be enough. If this is not done, the PATH
environment value might not contain the needed directories. Most modern Linux
systems place it into PATH
if it exists.
Bad programmers start from algorithms. Good programmers start from data structures. "Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious." An example of a tool created by a good programmer is Git, where the data model supports merges, being superior to CVS that does not.
Stirmake aims to have good data structures from day one. Specifically:
- Variables have proper data types; the data model is very similar to JSON
- There is recursive nested scoping; variables in sub-Stirfiles affect only sub-sub-Stirfiles included in the sub-Stirfiles, but not the main-Stirfile
- Delayed evaluation is created using functions
- Rules have 1..N targets and 0..M dependencies
- Each rule has 0..K commands to execute, with executed commands stored to database
As example of true data typing, see:
$SRC = ["foo.c", "bar.c", "baz.c"]
As example of delayed evaluation:
$CCCMD<> = ["cc", "-Wall", "-c", $<]
where the marker <>
means create a function from the expression.
As example of nested scoping:
@beginholeyscope
$CCCMD = @LP $CCCMD
@projdirinclude "subproject"
@endscope
Now $CCCMD
is visible to the subproject but $SRC
is not. The @LP
means
access lexical parent scope.
As example of rules, a Makefile is incapable of representing the following Stirfile because the rule has 2 targets:
stiryy.tab.c stiryy.tab.h: stiryy.y
byacc -d -p stiryy -o stiryy.tab.c stiryy.y
The best we can do with make is the following (ugh):
stiryy.tab.c: stiryy.y Makefile
byacc -d -p stiryy -o .tmpc.stiryy.tab.c stiryy.y
rm .tmpc.stiryy.tab.h
mv .tmpc.stiryy.tab.c stiryy.tab.c
stiryy.tab.h: stiryy.y Makefile
byacc -d -p stiryy -o .tmph.stiryy.tab.c stiryy.y
rm .tmph.stiryy.tab.c
mv .tmph.stiryy.tab.h stiryy.tab.h
Further minor details of the data model:
- Dependency can be: (i) normal, (ii) recursive, (iii) order-only
- Rule can be: (a) normal, (b) recursive target rule, (c) phony, (d) maybe-rule, (e) dist-rule
Recursive dependencies depend on the latest mtime within an entire hierarchy. Order-only dependencies are executed only if the file/directory does not exist, and a changed mtime does not cause execution of the rule.
Recursive target rules depend on targets inside a recursive dependency hierarchy. Example:
@rectgtrule: subproj/bin/cmd subproj/lib/libsp.a: @recdep subproj
make -C subproj
The @rectgtrule is executed whenever either subproj/bin/cmd
or
subproj/lib/libsp.a
is older than other files within the hierarchy. However,
if the rule is executed, the files subproj/bin/cmd
and subproj/lib/libsp.a
are automatically touched if the sub-make didn't touch them so that subsequent
invocations do not execute the rule anymore. Note the condition that files are
not touched by sub-make can happen if one does touch subproj/README.txt
which
obviously does not cause the sub-make to do anything.
Phony-rules are well-known from make.
Maybe-rules do not check that the target is always updated. Example:
@mayberule: foo: Makefile.foo foo.c
make -f Makefile.foo
Dist-rules mean the rule creates a final binary ended for end-user and not some intermediate object file. The only difference it has is for automatically cleaning object files and binaries: dist-rule creates a binary instead of object file.
One can include Stirfiles of subdirectories with the @dirinclude
directive,
and the Stirfiles of subprojects with the @projdirinclude
directive. It is
recommended to use @projdirinclude
with a @beginholeyscope
so that only
defined variables are visible to the subproject. An example:
@dirinclude "subdir1"
@dirinclude "subdir2"
@beginholeyscope
$CCCMD = @LP $CCCMD
@projdirinclude "subproject"
@endscope
Note that each Stirfile must begin with either @toplevel
or @subfile
depending on whether it's the top-level Stirfile of a project, or a Stirfile
belonging to a subdirectory of a project.
Suppose there is the following hierarchy:
project/Stirfile
project/dir/Stirfile
project/dir/subproj/Stirfile
project/dir/subproj/subdir/Stirfile
...and suppose subproj
is a git submodule.
Suppose one is currently at project/dir/subproj/subdir/
.
Then one invokes stirmake
as stirmake -a
(synonym: smka
) to build
something relative to the entire project hierarchy, or stirmake -p
(synonym:
smkp
) to build something relative to the subproject, or stirmake -t
(synonym: smkt
) to build something relative to the current directory.
As an example, the following are equal:
cd project/dir/subproj/subdir; smkt ../all
cd project/dir/subproj/subdir; smkp all
cd project/dir/subproj/subdir; smka project/dir/subproj/all
where it is assumed that all of the Stirfiles include a phony target all
.
If the target is not given, stirmake automatically uses the first target within the project hierarchy / subproject / subdirectory.
Cleaning should never be necessary. However, stirmake automatically figures out the rules for cleaning object files and binaries. To clean, do some of these:
smka -bc
: clean binaries and object files of whole project hierarchy, then exitsmka -b
: clean binaries of whole project hierarchy, then exitsmka -c
: clean object files of whole project hierarchy, then exitsmka -bc all
: clean binaries and object files of whole project hierarchy, then build phony targetall
smkp -bc
: clean binaries and object files of whole projectsmkt -bc
: clean binaries and object files of current directory
To add hooks for cleaning, do:
@cleanhook:
make -C subdir clean
@distcleanhook:
make -C subdir binclean
@bothcleanhook:
make -C subdir clean binclean
If the sub-Makefile does not support cleaning only binaries and not object
files, you can set one of the hooks to false
to fail the operation:
@cleanhook:
make -C subdir clean
@distcleanhook:
false
@bothcleanhook:
make -C subdir clobber
The hooks are recursively executed, and may even have dependencies. There are implicit dependencies so that clean hooks of sub-Stirfiles are executed before the clean hooks of parent-Stirfiles are executed.
Parallel build uses the familiar syntax: stirmake -j8
, but with the exception
that CPU count autodetection is supported: stirmake -ja
.
Stirmake has a debug mode that is enabled by using the -d
command line
argument. It is very verbose and explains what stirmake does and why it does
that.
The C programming language has a header file inclusion mechanism that might make a C source file depend on numerous header files. It would be way too cumbersome to document every header dependency in a Makefile, so that's why C compilers typically support outputting automatically created dependencies in a format supported by make.
In Make, one typically compiles dependencies in the following manner:
$(DEP): %.d: %.c
$(CC) $(CFLAGS) -MM -MP -MT "$*.d $*.o" -o $*.d $*.c
Note that -MP
option is required to make compiling work after a header file
has been deleted or renamed. It creates a phony dependency for every header
file. Note also that the rule to create the dependencies must also have all
headers as dependencies, so the -MT "$*.d $*.o"
option is needed to make not
only the object file but also the dependency file dependent on all headers.
Then when the dependencies have been compiled, they are imported in make as follows:
-include *.d
In Stirmake, there is already support for creating the phony rules automatically on the fly to allow compiling to work after header file rename or deletion. Also there is automatic support to make the dependency file also depend on the headers. Thus, dependencies are created as follows:
$(SRC) = ["file1.c", "file2.c", "file3.c"]
$(DEP) = @sufsuball($(SRC), ".c", ".d")
@patrule: $(DEP): '%.d': '%.c'
@ [$(CC), @$(CFLAGS), "-MM", "-o", $@, $<]
And imported as follows:
@cdepincludes @autophony @autotarget @ignore [@$(DEP)]
Note here @autophony
that replaces the -MP
option and @autotarget
that
replaces the -MT "$*.d $*.o"
option. Note also @ignore
that causes missing
files to be ignored, so that compiling works in a freshly cloned or cleaned
directory. Usually all of @autophony
, @autotarget
and @ignore
should be
used.
Stirmake automatically stores a list of commands used to build targets into the
file .stir.db
. Whenever a command fails, it is removed from .stir.db
;
whenever a command succeeds, it is added/updated to .stir.db
.
All rules depend on the exact command used to build the targets. If the command has changed, a re-build for the rule is done even though all targets may be up-to-date based on mtime.
Because of this property, stirmake
should never require make clean
, and one
does not need an explicit dependency on Stirfile
for all rules.
Stirmake automatically integrates with GNU make jobserver. Several tricks are done to get non-blocking behavior on a blocking file descriptor. Stirmake can be a jobserver host or a jobserver guest. Commands are automatically compared to a built-in list of commands that represent sub-makes:
make
gmake
/usr/bin/make
/usr/bin/gmake
/usr/local/bin/make
/usr/local/bin/gmake
/usr/pkg/bin/make
/usr/pkg/bin/gmake
/opt/bin/make
/opt/bin/gmake
/opt/gnu/bin/make
/opt/gnu/bin/gmake
/bin/make
/bin/gmake
If the command is detected to be a sub-make, the MAKEFLAGS
environment
variable is set to contain the jobserver details.
Stirmake is programmed by a custom programming language Amyplan using the bytecode engine abce. It is a strongly and dynamically typed interpreted programming language.
There are several reasons why a custom language is used:
- The use of a language where every reserved word begins with the sigil
@
allows smoothly embedding the embedded language syntax into Stirfiles - A custom language can support recursive nested scoping creatable from the C API, which is something that e.g. Lua lacks
- The custom language allows compiling variable assignments with delayed evaluation to bytecode
To see documentation of Amyplan, go to stirc/abce
directory of the project.
There are several warnings and helpful suggestions stirmake may emit. This section explains those in detail.
Recommend using string literals instead of free-form tokens
This means one should instead of:
foo.o: foo.c
cc -o foo.c
Do this:
"foo.o": "foo.c"
cc -o foo.c
The reason is that stirmake has support for proper data types, so one should
use them. Later versions may change the behaviour of unquoted free-form tokens.
For example, the free-form tokens cause a problem for maximal munch tokenizing.
Answer quickly: is 4/2
a filename or a mathematical expression? What about 4 / 2
, then?
Recommend setting rule for X to @rectgtrule
A rule where some targets are inside a @recdep
dependency should be marked
@rectgtrule
for smooth operation of incremental build. Without specifying it
as @rectgtrule
, one can have a system that builds too much for subsequent
invocations of stirmake.
Recommend making directory dep X of Y either @orderonly or @recdep.
A dependency was detected to be a directory. Almost always, one should not
depend on the mtime of the directory (which means the last time a direct child
file was added or removed), but rather the recursive newest mtime within the
directory (@recdep
), or whether the directory exists at all (@orderonly
).
Can't find old symbol function for X
The $VAR += [...]
syntax can only be used if $VAR
is already defined.
var X not found
Functions must refer to non-local variable $X
using dynamic scope @D$X
or
lexical scope @L$X
.
Recursion misuse detected
Stirmake is designed to be used non-recursively by including sub-projects
instead of invoking a separate stirmake instance for sub-projects. If you
really want to invoke a sub-stirmake, please create a wrapper script that
un-sets the environment variable STIRMAKEPID
to allow recursive invocation.
ruleid by tgt X already exists
You tried to create the same rule twice. You can specify a rule only once, but
additional dependencies can be specified using @deponly: tgt: add deps
.
cycle found, cannot proceed further
The dependencies have a cycle. Please break it.
target X was not created by rule
The rule must create/update its target. If this does not always happen, you
must mark it @mayberule
or @phonyrule
. Also, @rectgtrule
may be used for
rules that have targets inside @recdep
.
target X was not updated by rule
The same.
No X and rule not found
The file X does not exist and there is no rule to make it
Can't lock DB. Other stirmake running? Exiting.
Stirmake was unable to obtain a lock on the command database. This means likely another stirmake instance is running for the same project hierarchy.
stirmake: syntax error at file Stirfile line 1 col Y.
This probably means you didn't start the Stirfile with the @toplevel
or the
@subfile
marker. Please select the correct marker and place it to the first
line of the Stirfile.
- Miller, P.A. (1998), Recursive Make Considered Harmful, AUUGN Journal of AUUG Inc., 19(1), pp. 14-25, http://aegis.sourceforge.net/auug97.pdf
- Mokhov, A., Mitchell, N., Peyton Jones, S., Marlow, S. (2016), Non-recursive make considered harmful: build systems at scale, ACM SIGPLAN Notices - Haskell '16, 51(12), pp. 170-181, https://www.microsoft.com/en-us/research/wp-content/uploads/2016/03/hadrian.pdf