It's worth highlighting here how Tup differs fundamentally from Make in that you specify the DAG from the bottom-up, rather than the top-down as in Make.

That is to say, with the top-down scheme used by Make you think about and write things down in this kind of order:

1. executable <- object files
2. object files <- source files

Whereas in Tup you think and write bottom-up, like this:

1. source files -> object files
2. object files -> executable

... which is more akin to how you would write a simple shell script for your build, with all of the commands in the order that they need to be executed. But you still get the time saving of files not being rebuilt if their sources haven't changed, which is the point of using a build system over a shell script in the first place.

To put it another way, Make is declarative like a functional language, whereas Tup more like an imperative language. If you're like me, building is a chore and a build system is supposed to make my life easier, so if it's very much more difficult than a shell script then I can't be bothered with it. Like most programmers I'm more at home with imperative languages than functional; and looking at other people's Makefiles tends to do my head in.

Having said that while Tup has less cruft and some better features (eg. multiple output files as per the parent poster's wish) than Make, it's still a domain-specific language and as such its overall features are limited to what its creator decided were sufficient to his definition of "building programs" for the sake of terser syntax. So, if you're like me you may prefer to hurry on to the 'ultimate' step which is to just use a general-purpose language with a helper library for running build commands. I currently use fabricate.py (http://code.google.com/p/fabricate/) which provides a 'run("shell command")' function to a Python script that runs the command while hooking system calls to spy on the command's inputs and outputs, then saves the command-line and the input/output file names to a file ('.deps') so next time it can check whether the command needs to be re-run or not.

Of course in a general-purpose language such as Python you can do just about anything you like and don't have problems with silly little things like spaces in strings (though see below). While the resulting script may not be as short as the 'ideal' Makefile or Tupfile which could be nearly empty with all their implicit rules taken into account, I'm of the view that explicit is better than implicit.

Couple of specific fabricate.py caveats - being on Windows, I had to do some hackery on my copy of the library to make the dependency detection work better, and to allow for supplying the dependency filenames manually as a fallback. And, the documentation of that 'run()' function could be better (in short, you shouldn't just supply a full string as you would type it at the command line; you should convert those space-seperated command-line arguments into comma-seperated Python arguments, and Fabricate will automatically quote any argument that contains a space. I have no connection with the project but I should really see if I can get into their wiki there and add a little doc laying out the subtleties of it; it does work out in the end). That said, I couldn't be much happier now; it works like a champ everywhere I've tried it including C++, Java, ActionScript and Haxe.

The other downside of bottom-up/imperative style is that it doesn't lend itself automatically to parallel builds (cf. an oft-noted difference between imperative and functional languages in general). I hear it's possible if you plan for it (http://code.google.com/p/fabricate/wiki/ParallelBuilding) though personally my projects have not been big enough to bother trying this so far.