xmk is another build automation tool inspired by Make. It features a hierarchical block structure and a function interface to facilitate Make-like dependency resolution.It aims to be simple and explicit even in projects that are meant to be compiled for more than one platform.
Implementing a parser for the language was less trivial than I had expected, but I've managed to hack together a working prototype over the last couple days. You can check it out by installing its Ruby gem: gem install xmk
. Unfortunately there is already a build tool that has been coined xmake, so I'll either have to find another name for it or stick with its prototype shortname. But that won't be relevant for a while because even the next prototype version is going to take a good while to complete. Anyways, here's how it works so far:
xmk uses three significant blocks to perform build actions (4 if you count root space). It starts in root, jumps into the first of the specified targets it finds, and then parses the relevant platforms one after another. Labels can be used throughout to further control the build process. Used classically each target is a binary that is meant to be compiled for one or more platforms. You can control variables to adjust options for each binary inside of every control layer.
The program begins processing in root space. When it jumps into a target block it creates copies of every previously set variable. Changes made to variables in the target space only affect these copies. When the program later jumps into a platform block it copies these variables again. When returning to the target space the latest copies are discarded and the program returns to its state from the moment it entered a platform block. The target state becomes the origin state for every platform that is being compiled for. Like Make xmk is primarily meant to be used in conjunction with C and C++ compilers, but its flexible design (that is aimed at for release version 1.0) should make it useful for a wider variety of build cases.
Prototype overview
The current design and implementation for xmk is at a very rough prototype state. Later versions won't be compatible with these xmkfiles, but it was important to me to get a proof of concept out of the window. The files the prototype implementation can read look and work the following way. As stated before the program starts in root space and eventually jumps into a target block which is specified with a preceding @
.
001 SRC = src/*.c # file names with wildcards get implicitly expanded to a list of all
002 # matching filenames during compilation
003
004 @myapplication # the target binary
005 SRC += src/math/*.c
006 BIN = bin
007 INC = -Iinclude
008 LIB = -lm
009 ...
010
So when myapplication
is built it has access to the value set to the SRC
variable. Within target blocks labels can be used to further control the compilation flow. Labels are active when set via command line arguments. For example:
001 @myapplication
002 BIN = bin
003 CFLAGS = -O2
004
005 debug:
006 BIN += /debug
007 CFLAGS += -D_DEBUG
008
To run the debug label you invoke the program like xmk -t myapplication -l debug
. Commands are only accepted in label scope, so xmk features default labels that are invoked on every call. These always start with an underscore character _
:
001 @myapplication
002
003 _cmd:
004 echo Hello World!
005
Newlines are significant in the current build. That's how the program knows that it exits a scope. The special _plat
label makes the program jump to the specified platform blocks. If no platform was given as argument it will run all platforms that are associated to a target. Likewise if no target is specified the program will attempt to build all targets. Platform blocks make further use of built-in labels to realize the compilation process:
001 linux: @myapplication
002 CC = gcc
003 BIN += /linux
004 OBJ += obj/linux
005
006 debug:
007 ...
008
009 _rule: # this is run for each source file. it implicitly creates the directory strucure needed to
010 # store the object files and calls its command for every source file in SRC
011 # BIN and OBJ are implicitly required variables
012 # $(VAR) is replaced by the corresponding variable and $~ is replaced with the source file of
013 # the current pass
014 $(CC) -M $~ $(CFLAGS) $(INC)
015
016 ...
017
To complete the compilation processes two additional keyword labels have to be used: compile
and link
.
001 linux: @myapplication
002 ...
003 OBJS = obj/linux/main.o obj/linux/math.o
004
005 _compile: # this command is run for each source file that requires to be recompiled. The dependency
006 # check happens implicitly in the background
007 # again $~ is replaced with the current source file, likewise $& is replaced with its corre-
008 # sponding object file location
009 $(CC) -c $~ -o $& $(CFLAGS) $(INC)
010
011 _link: # here $&& is replaced with a list of all object files
012 $(CC) -o $(BIN) $&& $(LIBS)
013
014 # custom labels can be used to run further commands
015
016 clean: # run if invoked with -l clean
017 rm $(OBJS)
018
The commands calling gcc are flexible of course, as long as they get their jobs done for each label (getting a dependency recipe, compiling individual source files, linking object files together). After processing the platform state is discarded so that the next platform can run with the configs defined in the target block. Here's a complete working example of an xmkfile that I've confirmed to work:
001 @geneiseidenki
002 SRC=src/main.c src/input.c src/helper/*.c
003 SRC+= src/saturn_engine/*.c src/saturn_engine/core/*.c src/scene/*.c
004 CFLAGS=-Iinclude -Imruby-2.1.0/include
005 LIBS=-lm
006
007 debug:
008 CFLAGS+= -D_STN_DEBUG
009 echo "running debug for @$@"
010
011 _plat: # executes platforms
012
013 verbose:
014 echo "finished building @$@"
015
016 linux: @geneiseidenki
017 CC=gcc
018 OBJ=obj/linux
019 BIN=bin/$@-$! # $! replaced by platform
020 CFLAGS+= `sdl2-config --cflags`
021 LIBS+= `sdl2-config --libs` -lSDL2_image \
022 -lSDL2_mixer mruby-2.1.0/build/host/lib/libmruby.a
023
024 debug:
025 CFLAGS+= -g -Wall -Wno-switch -Wno-unused-label
026 OBJ=obj/linux/debug
027 BIN=bin/$@-$!-debug
028
029 _pre:
030
031 _rule:
032 $(CC) -M $~ $(CFLAGS) # $~ replaced with given src file
033
034 _compile:
035 $(CC) -c $~ -o $& $(CFLAGS) # $& replaces .$(SRC_EXT) with .o in $~
036
037 _link:
038 $(CC) -o $(BIN) $&& $(LIBS) # $&& expands to list of all object files
039
040 _post:
041 chmod +x $(BIN)
042
043 win64: @geneiseidenki
044 CC=x86_64-w64-mingw32-gcc
045 OBJ=obj/win64
046 BIN=bin/$@-$!.exe
047 CFLAGS+= -Ilib/SDL2-2.0.9/x86_64-w64-mingw32/include/SDL2 \
048 -Ilib/SDL2_image-2.0.4/x86_64-w64-mingw32/include/SDL2 \
049 -Ilib/SDL2_mixer-2.0.4/x86_64-w64-mingw32/include/SDL2
050 LIBS+= -lmingw32 -lSDL2main -lSDL2 -lSDL2_image -lSDL2_mixer \
051 -Llib/SDL2-2.0.9/x86_64-w64-mingw32/lib \
052 -Llib/SDL2_image-2.0.4/x86_64-w64-mingw32/lib \
053 -Llib/SDL2_mixer-2.0.4/x86_64-w64-mingw32/lib \
054 mruby-2.1.0/build/win64/lib/libmruby.a -lws2_32
055
056 debug:
057 OBJ=obj/win64/debug
058 BIN=bin/$@-$!-debug.exe
059
060 _pre:
061
062 _rule:
063 $(CC) -M $~ $(CFLAGS) -D_WIN32
064
065 _compile:
066 $(CC) -c $~ -o $& $(CFLAGS) -D_WIN32
067
068 _link:
069 $(CC) -o $(BIN) $&& $(LIBS)
070
071 _post:
072
Anyways, it's just a prototype. The next iteration is planned to be more explicit in how it works and what it does. If it goes well future xmk build descriptions will likely look like this:
001 default(-t my_application -p linux -s debug) # if run without arguments
002 list[] src = files(src/*.c, src/scene/*.c, src/engine/*.c) # delimiter is optional
003 src = filesr(src, *.c) # recursive variant with pattern to match in each directory
004 var obj_dir = obj/
005 var cc
006 var bin = bin/
007 list[,] inc = -Iinclude
008 list[,] lib
009 list[,] cflag
010 list[,] sflag
011 list[,] dep
012 if debug
013 cflag << -g, -Wall, -Wno-switch, -Wno-unused-label
014 sflag << -DDEBUG
015 { echo Debug Mode! } print if verbose (switches can determine if cmd output should be printed)
016 { my_cmd }
017 end
018
019 @my_application
020 # since this example has only one target the above might as well be listed inside of
021 # the @my_application scope
022 process() # jumps into platform scopes that have been called to be built
023 end
024
025 action linux
026 lib << list({ `sdl2-config --libs` }, ), lSDL2_image, lSDL2_mixer
027 inc << -Imruby-2.1.0/include
028 obj_dir << linux
029 cc = gcc
030 bin << linux
031 if debug
032 bin << -debug # += operator would preserve whitespace when appending strings
033 else
034 # nothing yet
035 end
036 list[] obj = from_src(src, obj_dir) # this function also creates the directory structure required
037 # to host the objects
038 for(src, obj) # yes, foreach loops that can loop many lists at once
039 dep = dependencies_for({ $(cc) -M $1 $(cflags) $(inc) })
040 if dependency_changed($2, $1) # obj ($2) depends on files ($1)
041 { $(cc) -c $1 -o $2 $(cflags) $(inc) } print if verbose
042 end
043 end
044 { $(cc) -o $(bin) $($obj, ) $(lib) } print if verbose
045
046 end
047
But that's for another day. As is syntax highlighting. :)
For the record, the next version will likely come with helper functions to write header files ahead of compilation. But I haven't thought about the specifics of that just yet.