SNAKE: The unholy offspring of make and Python

Snake is a build utility implemented in Python 2 with features similar to many versions of make. This implementation uses GNU's version make as its guiding influence.

Snake's operation is determined by a file called a "snakefile" containing arbitrary Python code, plus make-like rules. The implementation is as a preprocessor which turns rules into Python code; the processed buffer is then interpreted by Python.

Many of the features of GNU make would add little to what Python can already do. The features borrowed from make are therefore relatively few.

Snake can be used by other Python code as an imported module, and the build rules set by Python code, or as a command line utility using a snakefile in a manner quite similar to GNU make and its makefile.

SNAKEFILE

When used from the command line, snake attempts to load a file called "Snakefile" or "snakefile" in the current directory, unless a different snakefile is specified as a command line parameter. The snakefile looks similar to a Python source file, with the addition of zero or more "rules", as explained below. Snake requires the sys and os modules to operate, so it implicitly imports them. Therefore it is unneccessary to import there modules in the snakefile. In addition, the global symbol "snake" will be available within the code in the snakefile. It is an instance of the class snake.Snakefile.

The command-line parameters understood by snake are similar to those known to GNU make. Execute snake with the '-h' flag to see usage information.

RULES

Rules have syntax very similar to that of GNU make. In general, they take the form

  targets : prerequisites
    command
    ...

The commands are simply arbitrary Python code to be executed to build the targets from the prerequisites. A rule says that to bring the listed targets up to date, recursively bring the prerequisites up to date. If any of the prerequisites required action, or are newer than the target, or the target doesn't exist, the list commands are performed.

The normal indentation rules of Python determine what is part of the action for a rule. The first target of rule may not be a word reserved by Python for compound statement syntax ('if', 'for', 'try', etc.).

SHELL-LIKE SUBSTITUTION

The rule line "targets : prerequisite" is treated specially in that all occurrences of "$name" within the line will replaced by the name variable in the target or prerequisite. For example,

  for src in ('a', 'b'):
    $src.o: $src.c util.c
      cc.compile(snake.prerequisites)

is equivalent to

  a.o: a.c util.c
    cc.compile(snake.prerequisites)

  b.o: b.c util.c
    cc.compile(snake.prerequisites)

Also, '$(expr)' or '${expr}' will be substituted with the value of the given expression, which may not contain parentheses or curly braces, respectively. The string "$$" may be used to represent a single dollar sign in contexts where it would be interpreted as a substitution.

SHELL COMMANDS

Other than rule lines, the rest of the snakefile is treated as Python code, with one exception. When the first nonblank character of a line is the '!' character, the remainder of the line (possibly extended by a terminating '\') is executed as a shell command (using sys.command()).

For example,

  CC = 'gcc -c'
  for src in ('a', 'b'):
    $src.o: $src.c util.c
      !$(CC) ${snake.prerequisites.join()}

Notice that $-substitution also operates on shell commands (prior to sending the command to the shell).

PATTERN RULES

Simple pattern rules are supported as well. The appearance of a single '%' character in any target matches any target which can be formed by replacing the '%' with one or more characters. When this is the case, the appearance of a '%' in a prerequisite is replaced by the corresponding stem in the target. For example:

  %.o: %.c util.c
    cc.compile(snake.prerequisites)

subsumes the two rules in the section above (and many more besides).

If more than one pattern rule would match a target, the one first defined is used. However if an explicit rule (one without a '%') matches the target, that rule take precedence.

IMPLICIT VARIABLES

GNU make has several implicit variables, which snake does not. Several fields of the globally defined object snake can be used within actions instead:

    field               make var meaning
    ------------------  -------  --------------------------
    snake.target          $@     current target for action
    snake.prerequisites   $+     all prerequisites for target
    snake.newer           $?     those of above which were newer
    snake.stem            $*     stem for pattern replacement

OBJECT INTERFACE

The Snakefile object exposes several methods which can be used instead of, or in conjuction with the make-like rules.

For example, the effects of this rule:

  %.o: %.c util.c
    cc.compile(snake.prerequisites)

could be implemented via:

  snake.depend('%.o', '%.c', 'util.c')
  snake.action('%.o', 'cc.compile(snake.prerequisites)')

Function pointers may also be specified as actions.

Outside of a snakefile, one could use this code:

  s = Snakefile()
  s.depend('%.o', '%.c', 'util.c')
  s.action('%.o', 'cc.compile(snake.prerequisites)')
  s.make('junk.o')

which would cause the call to cc.compile to take place, if junk.o is out of date. Note that the variable snake is still bound within an action in this context.

MISCELLANEOUS FEATURES

- The .PHONEY target is supported as in GNU make. See make's documentation for further explanation.

- Wildcard characters (*, ? and [...]) are supported in dependency lists, as in

  snake.depend('main.c', '*.h')

- Rules may appear within an action. This enables a target to select a rule or set of rules for later targets. This will probably rarely be useful, however.