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
004  @myapplication # the target binary
005      SRC += src/math/*.c
006      BIN = bin
007      INC = -Iinclude
008      LIB = -lm
009      ...

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 
005      debug:
006          BIN += /debug
007          CFLAGS += -D_DEBUG

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
003      _cmd:
004          echo Hello World!

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
006      debug:
007          ...
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)
016      ...

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
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)
011      _link:    # here $&& is replaced with a list of all object files
012          $(CC) -o $(BIN) $&& $(LIBS) 
014                # custom labels can be used to run further commands
016      clean:    # run if invoked with -l clean
017          rm $(OBJS)

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
007      debug: 
008          CFLAGS+= -D_STN_DEBUG 
009          echo "running debug for @$@"
011      _plat: # executes platforms
013      verbose:
014          echo "finished building @$@"
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
024      debug:
025          CFLAGS+= -g -Wall -Wno-switch -Wno-unused-label
026          OBJ=obj/linux/debug
027          BIN=bin/$@-$!-debug
029      _pre:
031      _rule:
032          $(CC) -M $~ $(CFLAGS) # $~ replaced with given src file
034      _compile:
035          $(CC) -c $~ -o $& $(CFLAGS) # $& replaces .$(SRC_EXT) with .o in $~
037      _link:
038          $(CC) -o $(BIN) $&& $(LIBS) # $&& expands to list of all object files
040      _post:
041          chmod +x $(BIN)
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
056      debug:
057          OBJ=obj/win64/debug
058          BIN=bin/$@-$!-debug.exe
060      _pre:
062      _rule:
063          $(CC) -M $~ $(CFLAGS) -D_WIN32
065      _compile:
066          $(CC) -c $~ -o $& $(CFLAGS) -D_WIN32
068      _link:
069          $(CC) -o $(BIN) $&& $(LIBS)
071      _post:

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
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
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
046  end

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.