Mk the better Make

⚡︎
Warning

This is not a tutorial about Mk, although the article is written with the intention to help understand its design that are not explicitly listed in manual and the paper.

1Introduction

Back in the days when I was playing with MetaPRL, the project was built using OMake, a Make like build system dictated to MetaPRL project. OMake has very advanced recipe language that supports functional style programming, and novel features like able to handle complex dependency relation, custom defined action when build fails, subdirectory management, and using hashing function instead of time stamp to test if a file is changed. Although OMake needs OCaml tool chain to build and cannot be installed standalone, and sometimes it bugged because been installed multiple versions at same time, it is still a decent general purpose building system that I would like to have.

That was all before I actually learned and used Unix make, and later I discovered it is almost impossible to define anything close to OMake's abstraction using Make or GNUMake without littering the file system with touched empty files to track dependency. I struggled, but finally I gave up.

That was the situation until I meet Mk that is being used on Plan 9. Unlike OMake which has all sorts of fancy features and even maintains a database at the project root, Mk still relies on simple features like file time stamps, although this time you also have an attribute to specify custom file testing method.

2How Mk is (fundamentally) different from Make

The first interesting thing is Mk evaluates the mkfile in order, and create the dependency graph in the memory after expended variables, the recipes are however uninterpreted and will be directly passed to shell verbatim and using inherited shell variables to obtain information such like file names. During this process a shell of your choice would be used to interpret the things for meta programming, and you can switch them in between.

Unlike Make which failed at giving a reasonable semantic to a rule having multiple targets, Mk can capture the notion that a recipe could produce multiple targets that needs to be tracked very well. A rule without recipe only adds additional dependency relation, to complement meta rules. A rule without dependency describes one or multiple targets that would produce by running the recipe, and the dependency of each individual target can be added as additional rules.

3Tricks and Pitfalls

The OMake hashing feature can be mimic by the YACC example in Mk man page, you still need to store an extra copy of the file, or at least the checksum, however that's also what OMake need to maintain. In case you don't understand why need hashing in addition to time stamp, that's because a file could be regenerated as a side effect, but the content could be unchanged, and detecting that can save building time.

The regexp metarule is such an obscure feature that no one has yet mentioned somehow the Unix port of Mk need the prerequisites to use double backslashes instead of one, due to different escaping rules in shell. The original Mk paper Hume did use single quote like '\1/\2.c' to prevent shell interpretation of backslashes. Here is another example.

  |$DESTDIR/([^/.]+)\.(png|mp3|svg|jpg):R: media/\\1.\\2
  |  cp $prereq $target

In my opinion this a useful feature for checking dependencies that spanning multiple directories.

Having multiple targets in one rule and able to declare them as virtual enables parallelized execution of "PHONY" targets, which would be a useful thing for automating testing. Here is an example that I use to validating DocBook XML.

  |%-valid:VQ: %.dbk
  |  echo "  JING" $stem
  |  $JING $SCHEMA $stem.dbk
  | 
  |validate:V: ${DOCBOOKS:%.dbk=%-valid}

When the times stamp of the target is identical to the prerequisites', Mk would try to update the target. This could unfortunately been the case if a fast command like copy has been used to update the target and the file system does not use high enough resolution for time stamps. If that happens to you, try to add a sleep command to extend the time. The Plan 9 sleep command also cannot do milliseconds level sleep and a float argument would be rounded so remember to use something like u sleep 0.1 to call the Unix version instead if you want finer control of it.

The custom compare command set by P can be a local shell script file, and it allows additional switches to be supplied. The target would be then appended to the command and the rest would be the dependencies. Mk would also require the dependencies to be present even though the compare script can ignore the dependencies. Also it is implied P must be the last of the attributes.

AThe Plan 9 regexp

I fully understand how frustrated it could be as a beginner when looking for living examples of a programming language but ended up found only BNFs.

However I can assure you that this is just a simplified version of Unix regexp that without special Character Classes, so you can just find any other tutorial as a replacement, or you can even only look for tutorials on how to use grep. Or maybe this article Regular Expression Matching Can Be Simple And Fast can help you appreciate the Plan 9 regexp more.

Bibliography

[Hume] Mk: A Successor to Make. Andrew G. Hume. http://doc.cat-v.org/bell_labs/mk/ .

Maintaining Files on Plan 9 with Mk. http://doc.cat-v.org/plan_9/4th_edition/papers/mk .

OMake. http://projects.camlcity.org/projects/omake.html .

mk – maintain (make) related files. https://9fans.github.io/plan9port/man/man1/mk.html .

regexp – Plan 9 regular expression notations. https://9fans.github.io/plan9port/man/man7/regexp.html .

[Regexp1] Regular Expression Matching Can Be Simple And Fast. (but is slow in Java, Perl, PHP, Python, Ruby, ...). Russ Cox. https://swtch.com/~rsc/regexp/regexp1.html .