diff --git a/CHANGES.txt b/CHANGES.txt
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,60 @@
 Changelog for Shake
 
+0.15.6
+    Require the primitive package
+    #431, make it an error to need a directory
+    #393, add orderOnlyAction, a general version of orderOnly
+    #408, explain that alwaysRerun has no immediate effect
+    #410, add --digest-not flag to select ChangeModtime
+    #416, export ShakeValue from the main module only
+    #352, improve the grammar in the profiling document
+    #399, explain what in getDirectoryFiles is tracked
+    #108, add getDirectoryFilesIO, an untracked version
+    #401, document getDirectoryFiles exception behaviour
+    #401, fix getDirectoryFiles error on pattern directory missing
+    #400, detect indirect recursion in build rules
+    #402, give better errors on recursive rules
+    #369, clarify the docs for putQuiet/putLoud
+    #405, make sure phony targets with \ work on Windows
+    Require extra-1.4.3
+    #398, only require old-time with GHC 7.4
+    #239, make Rules an instance of MonadIO
+    #384, add shakeExtra field to ShakeOptions for user data
+    #374, make --debug=file write out newlines
+    #308, make sure Shell and AutoDeps work together
+    #365, fix unicode output with putNormal
+    #269, add parallel execution via parallel/forP/par
+    #310, preserve quoting around cmd
+    #333, optimise the random pool, up to 20% faster
+    #335, add deriving Read instances for more types
+    Add Development.Shake.Forward for forward-defined systems
+    #336, export :->
+    #337, fix the docs for &%>
+    #297, make <enter> run the query profile viewer
+    #305, make profiling work with newer Chrome
+    #327, Add AutoDeps feature
+    #325, add FileStdin to specify the stdin comes from a file
+    Make an empty Stdin different from no Stdin (inherit) in cmd
+    #320, add ** as a relative-only alternative to // in patterns
+    #283, change the semantics of removeFiles, no implicit deletes
+    #283, speed up getDirectoryFiles on patterns involving literals
+    Work with stack test
+    Add RemEnv cmd option
+    Fix a small space leak on Linux
+    Fix a space leak in profile summary information
+    Properly handle *//* in file pattern (at least one directory)
+    #303, allow ninja rule names to have dots in them
+    Properly handle /// in file pattern (a wildcard then /)
+    Make sure Windows separators work in |%> FilePattern rules
+    #296, fix multifile rules when using --digest-and-input
+    #286, document the relationship between getEnv and lookupEnv
+    #285, improve the documentation for phony/alwaysRerun
+    #284, fix the documentation for normaliseEx
+    #293, add getProgress
+    #270, support MonadFix for Rules, allows recursive cache
+    #295, discount time spent waiting for a cache
+    #294, give better error messages if you hit GHC bug #10793
+    #4, add a lock file to stop corrupting databases
 0.15.5
     #279, make usingConfigFile do a need on the config file
     Fix a bug where predicted progress could sometimes be ??
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright Neil Mitchell 2011-2015.
+Copyright Neil Mitchell 2011-2016.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,25 +1,19 @@
-# Shake [![Hackage version](https://img.shields.io/hackage/v/shake.svg?style=flat)](https://hackage.haskell.org/package/shake) [![Build Status](https://img.shields.io/travis/ndmitchell/shake.svg?style=flat)](https://travis-ci.org/ndmitchell/shake)
+# Shake [![Hackage version](https://img.shields.io/hackage/v/shake.svg?label=Hackage)](https://hackage.haskell.org/package/shake) [![Stackage version](https://www.stackage.org/package/shake/badge/lts?label=Stackage)](https://www.stackage.org/package/shake) [![Linux Build Status](https://img.shields.io/travis/ndmitchell/shake.svg?label=Linux%20build)](https://travis-ci.org/ndmitchell/shake) [![Windows Build Status](https://img.shields.io/appveyor/ci/ndmitchell/shake.svg?label=Windows%20build)](https://ci.appveyor.com/project/ndmitchell/shake)
 
 Shake is a tool for writing build systems - an alternative to make, Scons, Ant etc. Shake has been used commercially for over five years, running thousands of builds per day. The website for Shake users is at [shakebuild.com](http://shakebuild.com).
 
 #### Documentation
 
-* **Why use Shake?** Shake lets you write large robust build systems, which deal properly with generated source files and run quickly. If you are writing a custom build system of any moderate size (more than a few rules) you should use Shake. The advantages over other build systems are detailed in the document [Why choose Shake?](https://github.com/ndmitchell/shake/blob/master/docs/Why.md#readme).
-* **How do I use Shake?** Shake is a Haskell library that you use to define your rules. The [Shake manual](https://github.com/ndmitchell/shake/blob/master/docs/Manual.md#readme) provides a walk through of a small but realistic example, assuming no Haskell knowledge.
+* **Why use Shake?** Shake lets you write large robust build systems, which deal properly with generated source files and run quickly. If you are writing a custom build system of any moderate size (more than a few rules) you should use Shake. The advantages over other build systems are detailed in the document [Why choose Shake?](http://shakebuild.com/why).
+* **How do I use Shake?** Shake is a Haskell library that you use to define your rules. The [Shake manual](http://shakebuild.com/manual) provides a walk through of a small but realistic example, assuming no Haskell knowledge.
 * [Generated documentation](http://hackage.haskell.org/packages/archive/shake/latest/doc/html/Development-Shake.html) for all functions, includes lots of examples.
-* [Running Ninja builds](https://github.com/ndmitchell/shake/blob/master/docs/Ninja.md#readme) using Shake.
 * [Blog posts](http://neilmitchell.blogspot.co.uk/search/label/shake) detailing ongoing development work.
-* [Profile report demo](https://cdn.rawgit.com/ndmitchell/shake/35fbe03c8d3bafeae17b58af89497ff3fdd54b22/html/demo.html) explaining what the profile reports mean.
-* [Academic paper](http://community.haskell.org/~ndm/downloads/paper-shake_before_building-10_sep_2012.pdf) on the underlying principles behind Shake.
-* [Video](http://www.youtube.com/watch?v=xYCPpXVlqFM) of a talk introducing Shake.
+* [Academic paper](http://ndmitchell.com/downloads/paper-shake_before_building-10_sep_2012.pdf) on the underlying principles behind Shake.
 
 #### Other links
 
 * [Download the Haskell package](http://hackage.haskell.org/package/shake) from Hackage and install it using Cabal.
 * [Mailing list](https://groups.google.com/forum/?fromgroups#!forum/shake-build-system) for any questions/bugs/thoughts on Shake. If you need more information and aren't sure where to start, use the mailing list.
 * [Questions](http://stackoverflow.com/questions/tagged/shake-build-system) can be asked on StackOverflow with the tag `shake-build-system`.
-* [Bugs](https://github.com/ndmitchell/shake/issues) can be reported on the GitHub issue tracker.
-* [Source code](http://github.com/ndmitchell/shake) in a git repo, stored at GitHub.
-* Continuous integration with [Travis](https://travis-ci.org/ndmitchell/shake) and [Hydra](http://hydra.cryp.to/jobset/shake/master).
 
-Is your company using Shake? Write something public (even just a [tweet  to `@ndm_haskell`](https://twitter.com/ndm_haskell)) and I'll include a link at [on the website](http://shakebuild.com/#who-uses-shake).
+Is your company using Shake? Write something public (even just a [tweet to `@ndm_haskell`](https://twitter.com/ndm_haskell)) and I'll include a link [on the website](http://shakebuild.com/#who-uses-shake).
diff --git a/docs/Manual.md b/docs/Manual.md
--- a/docs/Manual.md
+++ b/docs/Manual.md
@@ -1,6 +1,6 @@
 # Shake Manual
 
-_See also: [Shake links](https://github.com/ndmitchell/shake#readme); [Why choose Shake](Why.md#readme); [Function documentation](http://hackage.haskell.org/packages/archive/shake/latest/doc/html/Development-Shake.html)_
+_See also: [Shake links](https://github.com/ndmitchell/shake#readme); [Why choose Shake](Why.md#readme); [Function documentation](https://hackage.haskell.org/packages/archive/shake/latest/doc/html/Development-Shake.html)_
 
 Shake is a Haskell library for writing build systems - designed as a replacement for `make`. This document describes how to get started with Shake, assuming no prior Haskell knowledge. First, let's take a look at a Shake build system:
 
@@ -35,10 +35,9 @@
 
 To run the example above:
 
-1. Install the [Haskell Platform](http://www.haskell.org/platform/), which provides a Haskell compiler and standard libraries.
-2. Type `cabal update`, to download information about the latest versions of all Haskell packages.
-3. Type `cabal install shake`, to build and install Shake and all its dependencies.
-4. Type `shake --demo`, which will create a directory containing a sample project, the above Shake script (named `Build.hs`), and execute it (which can be done by `runhaskell Build.hs`). For more details see a [trace of `shake --demo`](Demo.md).
+1. Install the [Haskell Stack](http://haskellstack.org/), which provides a Haskell compiler and package manager.
+3. Type `stack install shake`, to build and install Shake and all its dependencies.
+4. Type `stack exec -- shake --demo`, which will create a directory containing a sample project, the above Shake script (named `Build.hs`), and execute it (which can be done by `runhaskell Build.hs`). For more details see a [trace of `shake --demo`](Demo.md).
 
 ## Basic syntax
 
@@ -205,13 +204,13 @@
 
 #### List manipulations
 
-Many functions work with lists of values. The simplest operation on lists is to join two lists together, which we do with `++`. For example, `["main.c"] ++ ["constants.c"]` equals `["main.c","constants.c"]`.
+Many functions work with lists of values. The simplest operation on lists is to join two lists together, which we do with `++`. For example, `["main.c"] ++ ["constants.c"]` equals `["main.c", "constants.c"]`.
 
 Using a _list comprehension_ we can produce new lists, apply functions to the elements and filtering them. As an example:
 
     ["_build" </> x -<.> "o" | x <- inputs]
 
-This expression grabs each element from `inputs` and names it `x` (the `x <- inputs`, pronounced "`x` is drawn from `inputs`"), then applies the expression  `"_build" </> x -<.> "o"` to each element. If we start with the list `["main.c","constants.c"]`, we would end up with `["_build/main.o","_build/constants.o"]`.
+This expression grabs each element from `inputs` and names it `x` (the `x <- inputs`, pronounced "`x` is drawn from `inputs`"), then applies the expression  `"_build" </> x -<.> "o"` to each element. If we start with the list `["main.c","constants.c"]`, we would end up with `["_build/main.o", "_build/constants.o"]`.
 
 List expressions also allow us to filter the list, for example we could know that the file `"evil.c"` is in the directory, but should not be compiled. We can extend that to:
 
@@ -287,7 +286,7 @@
     mkdir -p _shake
     ghc --make Build.hs -rtsopts -with-rtsopts=-I0 -outputdir=_shake -o _shake/build && _shake/build "$@"
 
-This script creates a folder named `_shake` for the build system objects to live in, then runs `ghc --make Build.hs` to produce `_shake/build`, then executes `_shake/build` with all arguments it was given. The `-with-rtsopts` flag can be treated as magic - it instructs the Haskell compiler to turn off features that would otherwise steal CPU from the commands you are running.
+This script creates a folder named `_shake` for the build system objects to live in, then runs `ghc --make Build.hs` to produce `_shake/build`, then executes `_shake/build` with all arguments it was given. The `-with-rtsopts` flag instructs the Haskell compiler to disable "idle garbage collection", making more CPU available for the commands you are running, as [explained here](http://stackoverflow.com/questions/34588057/why-does-shake-recommend-disabling-idle-garbage-collection/).
 
 Now you can run a build by simply typing `./build.sh` on Linux, or `build` on Windows. On Linux you may want to alias `build` to `./build.sh`. For the rest of this document we will assume `build` runs the build system.
 
@@ -303,7 +302,7 @@
 * `build --help` will list out all flags supported by the build system, currently 36 flags. Most flags supported by `make` are also supported by Shake based build systems.
 * `build -j8` will compile up to 8 rules simultaneously, by default Shake uses 1 processor.
 
-Most flags can also be set within the program by modifying the `shakeOptions` value. As an example, `build --metadata=_metadata` causes all Shake metadata files to be stored with names such as `_metadata/.shake.database`. Alternatively we can write `shakeOptions{shakeFiles="_metadata"}` instead of our existing `shakeFiles="_build"`. Values passed on the command line take preference over those given by `shakeOptions`. Multiple overrides can be given to `shakeOptions` by separating them with a comma, for example `shakeOptions{shakeFiles="_build",shakeThreads=8}`.
+Most flags can also be set within the program by modifying the `shakeOptions` value. As an example, `build --metadata=_metadata` causes all Shake metadata files to be stored with names such as `_metadata/.shake.database`. Alternatively we can write `shakeOptions{shakeFiles="_metadata"}` instead of our existing `shakeFiles="_build"`. Values passed on the command line take preference over those given by `shakeOptions`. Multiple overrides can be given to `shakeOptions` by separating them with a comma, for example `shakeOptions{shakeFiles="_build", shakeThreads=8}`.
 
 <span class="target" id="progress"></span>
 
@@ -312,9 +311,9 @@
 One useful feature of Shake is that it can predict the remaining build time, based on how long previous builds have taken. The number is only a prediction, but it does take account of which files require rebuilding, how fast your machine is currently running, parallelism settings etc. You can display progress messages in the titlebar of a Window by either:
 
 * Running `build --progress`
-* Setting `shakeOptions{shakeProgress=progressSimple}`
+* Setting `shakeOptions{shakeProgress = progressSimple}`
 
-The progress message will be displayed in the titlebar of the window, for example `3m12s (82%)` to indicate that the build is 82% complete and is predicted to take a further 3 minutes and 12 seconds. If you are running Windows 7 or higher and place the [`shake-progress`](http://github.org/ndmitchell/shake) utility somewhere on your `%PATH%` then the progress will also be displayed in the taskbar progress indicator:
+The progress message will be displayed in the titlebar of the window, for example `3m12s (82%)` to indicate that the build is 82% complete and is predicted to take a further 3 minutes and 12 seconds. If you are running Windows 7 or higher and place the [`shake-progress`](https://github.org/ndmitchell/shake) utility somewhere on your `%PATH%` then the progress will also be displayed in the taskbar progress indicator:
 
 ![](shake-progress.png)
 
@@ -333,11 +332,9 @@
 
 <span class="target" id="profiling"></span>
 
-#### Profiling
-
-Shake features an advanced profiling feature. To build with profiling run `build --report`, which will generate an interactive HTML profile named `report.html`. This report lets you examine what happened in that run, what takes most time to run, what rules depend on what etc. There is a help page included in the profiling output, and a [profiling tutorial/demo](https://cdn.rawgit.com/ndmitchell/shake/35fbe03c8d3bafeae17b58af89497ff3fdd54b22/html/demo.html).
+#### Profiling and optimisation
 
-To view profiling information for the _previous_ build, you can run `build --no-build --report`. This feature is useful if you have a build execution where a file unexpectedly rebuilds, you can generate a profiling report afterwards and see why. To generate a lightweight report (about 5 lines) printed to the console run `build --report=-`. 
+Shake features an advanced profiling feature. To build with profiling run `build --report`, which will generate an interactive HTML profile named `report.html`. This report lets you examine what happened in that run, what takes most time to run, what rules depend on what etc. For a full explanation of how to profile and optimise a build system, including getting accurate timings and using Haskell profiling, see [the profiling and optimisation page](Profiling.md).
 
 #### Tracing and debugging
 
@@ -409,7 +406,7 @@
 
 #### Multiple outputs
 
-Some tools, for example [bison](http://www.gnu.org/software/bison/), can generate multiple outputs from one execution. We can track these in Shake using the `&%>` operator to define rules:
+Some tools, for example [bison](https://www.gnu.org/software/bison/), can generate multiple outputs from one execution. We can track these in Shake using the `&%>` operator to define rules:
 
     ["//*.bison.h","//*.bison.c"] &%> \[outh, outc] -> do
         let src = outc -<.> "y"
@@ -457,61 +454,3 @@
     liftIO $ launchMissiles True
 
 Most common IO operations to run as actions are already wrapped and available in the Shake library, including `readFile'`, `writeFile'` and `copyFile'`. Other useful functions can be found in `System.Directory`.
-
-#### Include files with Visual Studio
-
-While `gcc` has the `-MMD` flag to generate a Makefile, the Visual Studio compiler `cl` does not. However, it does have a flag `-showincludes` which writes the include files on stdout as they are used. The initial example could be written using `cl` as:
-
-    Stdout stdout <- cmd "cl -showincludes -c" [input] ["-Fo" ++ output]
-    need [ dropWhile isSpace x
-         | x <- lines stdout
-         , Just x <- [stripPrefix "Note: including file:" x]]
-
-The `stripPrefix` function is available in the `Data.List` module. One warning: the "including file" message is localised, so if your developers are using non-English versions of Visual Studio the prefix string will be different
-
-#### Generated imports
-
-The initial example compiles the C file, then calls `need` on all its source and header files. This works fine if the header files are all source code. However, if any of the header files are _generated_ by the build system then when the compilation occurs they will not yet have been built. In general it is important to `need` any generated files _before_ they are used.
-
-To detect the included headers without using the compiler we can define `usedHeaders` as a top-level function:
-
-    usedHeaders src = [init x | x <- lines src, Just x <- [stripPrefix "#include \"" x]]
-
-This function takes the source code of a C file (`src`) and finds all lines that begin `#include "`, then takes the filename afterwards. This function does not work for all C files, but for most projects it is usually easy to write such a function that covers everything allowed by your coding standards.
-
-Assuming all interesting headers are only included directly by the C file (a restriction we remove in the next section), we can write the build rule as:
-
-    "_build//*.o" %> \out -> do
-        let c = dropDirectory1 $ out -<.> "c"
-        src <- readFile' c
-        need $ usedHeaders src
-        cmd "gcc -c" [c] "-o" [out]
-
-
-This code calls `readFile'` (which automatically calls `need` on the source file), then uses calls `need` on all headers used by the source file, then calls `gcc`. All files have `need` called on them before they are used, so if the C file or any of the header files have build system rules they will be run. 
-
-#### Generated transitive imports
-
-The previous section described how to deal with generated include files, but only coped with headers included directly by the C file. This section describes how to extend that to work with generated headers used either in C or header files, even when used by headers that were themselves generated. We can write:
-
-    ["*.c.dep","*.h.dep"] |%> \out -> do
-        src <- readFile' $ dropExtension out
-        writeFileLines out $ usedHeaders src
-
-    "*.deps" %> \out -> do
-        dep <- readFileLines $ out -<.> "dep"
-        deps <- mapM (readFileLines . (<.> "deps")) dep
-        writeFileLines out $ nub $ dropExtension out : concat deps
-
-    "*.o" %> \out -> do
-        deps <- readFileLines $ out -<.> "c.deps"
-        need deps
-        cmd "gcc -c" [dropExtension out] "-o" out
-
-For simplicity, this code assumes all files are in a single directory and all objects are generated files are placed in the same directory. We define three rules:
-
-* The `*.c.dep` and `*.h.dep` rule uses `|%>`, which defines a single action that matches multiple patterns. The file `foo.h.dep` contains a list of headers directly included by `foo.h`, using `usedHeaders` from the previous section.
-* The `*.deps` rule takes the transitive closure of dependencies, so `foo.h.deps` contains `foo.h` and all headers that `foo.h` pulls in. The rule takes the target file, and all the `.deps` for anything in the `.dep` file, and combines them. More abstractly, the rule calculates the transitive closure of _a_, namely _a*_, by taking the dependencies of _a_ (say _b_ and _c_) and computing _a\* = union(a, b\*, c\*)_.
-* The `*.o` rule reads the associated `.deps` file (ensuring it is up to date) and then depends on its contents.
-
-The pattern of `*.deps` files occurs frequently, for example when linking Haskell files.
diff --git a/docs/Ninja.md b/docs/Ninja.md
--- a/docs/Ninja.md
+++ b/docs/Ninja.md
@@ -2,24 +2,24 @@
 
 _See also: [Shake links](https://github.com/ndmitchell/shake#readme); [Shake manual](Manual.md#readme)_
 
-Shake supports the .ninja file format used by the [Ninja tool](http://martine.github.io/ninja/). This document describes how to use Shake instead of Ninja, and why you might want to do so.
+Shake supports the `.ninja` file format used by the [Ninja tool](https://ninja-build.org/). This document describes how to use Shake instead of Ninja, and why you might want to do so.
 
 #### Installing Shake
 
-1. Install the [Haskell Platform](http://www.haskell.org/platform/), which provides a Haskell compiler and standard libraries.
+1. Install the [Haskell Platform](https://www.haskell.org/platform/), which provides a Haskell compiler and standard libraries.
 2. Type `cabal update`, to download information about the latest versions of all Haskell packages.
 3. Type `cabal install shake --global`, to build and install Shake and all its dependencies.
 4. Type `shake --help`, to verify that Shake was successfully installed.
 
 #### Running Shake
 
-Change to the directory you usually run Ninja from (where there is a `build.ninja` file) and instead of typing `ninja` type `shake`. Ninja defaults to guessing how many processors to use, while Shake defaults to only 1 processor, so you will probably want to run something like `shake -j4` to use 4 processors (with a number appropriate for your computer).
+Change to the directory you usually run Ninja from (where there is a `build.ninja` file) and instead of typing `ninja` type `shake`. Ninja defaults to guessing how many processors to use, while Shake defaults to only 1 processor, so you will probably want to run something like `shake -j` to use all processors or `-j4` to use 4 processors (with a number appropriate for your computer).
 
 The following Ninja options are available in Shake:
 
 * Print version is `--version` in Shake.
 * Change directory before building is `--directory` in Shake.
-* Specify the .ninja file is `--file` in Shake.
+* Specify the `.ninja` file is `--file` in Shake.
 * Parallelism is `-j` in Shake.
 * Avoiding starting new jobs if the load average is over a certain level is not currently supported in Shake.
 * Keep going until a number of jobs fail is best approximated by `--keep-going` which keeps going regardless of how many jobs fail.
@@ -31,7 +31,7 @@
 
 #### Additional features of Shake
 
-For people who are set up to run an existing .ninja build file, there are two features of Shake that may appeal:
+For people who are set up to run an existing `.ninja` build file, there are two features of Shake that may appeal:
 
 * If you build with `--report` the file `report.html` will be generated. Open that report file and you can see numerous details about the build - how good the parallel utilisation was, what changed to cause what to rebuild, summary statistics, a dependency graph and more. See the Help page in any generated report for more details.
 * If you build with `--progress` the console titlebar will display a predicted completion time, how many seconds until your build completes. The predicted time will be fairly inaccurate the first time round, but future runs are influenced by recorded timings, and can produce useful guesses.
@@ -39,7 +39,7 @@
 
 #### FAQ
 
-* If I get this working, or can't get it working because of a bug, do you care? Yes - please email the [mailing list](https://groups.google.com/forum/?fromgroups#!forum/shake-build-system).
+* If I get this working, or can't get it working because of a bug, do you care? Yes - please [raise an issue](https://github.com/ndmitchell/issues).
 * Is Shake compatible with all Ninja features? Shake has support for everything in the Ninja manual - including response files, deps, pools and restat. Shake does not yet support rebuilding a file if the command line changes (if people rely on this feature, I am happy to add it). I am unaware of any Ninja files that don't work, but would be surprised if there were not some corner cases that Shake gets wrong (but tell me, and I'll fix it).
-* Is Shake faster or slower than Ninja? I have one data point - compiling LLVM on Windows under mingw they both take the same time to compile initially, and Ninja takes 0.9s for a nothing to do build vs Shake at 0.8s. Shake is slower at parsing Ninja files, so if you have _huge_ .ninja files (e.g. Chromium) Shake will probably be slower. Shake does less work if you don't specify deps, which is probably why it is faster on LLVM (but you should specify deps - it makes both Shake and Ninja faster). As people report more results I am sure both Shake and Ninja will be optimised.
-* Why did you make Shake interpret .ninja files? There are a few reasons: 1) It seemed like fun. 2) The Ninja team have made both [CMake](http://www.cmake.org/) and [gyp](https://code.google.com/p/gyp/) both generate .ninja files, so Shake can now build far more projects. 3) Shake and Ninja are both designed to be fast, benchmarking them has already improved the speed of Shake.
+* Is Shake faster or slower than Ninja? I have one data point - compiling LLVM on Windows under mingw they both take the same time to compile initially, and Ninja takes 0.9s for a nothing to do build vs Shake at 0.8s. Shake is slower at parsing Ninja files, so if you have _huge_ `.ninja` files (e.g. Chromium) Shake will probably be slower. Shake does less work if you don't specify deps, which is probably why it is faster on LLVM (but you should specify deps - it makes both Shake and Ninja faster). As people report more results I am sure both Shake and Ninja will be optimised.
+* Why did you make Shake interpret `.ninja` files? There are a few reasons: 1) It seemed like fun. 2) The Ninja team have made both [CMake](http://www.cmake.org/) and [gyp](https://code.google.com/p/gyp/) both generate `.ninja` files, so Shake can now build far more projects. 3) Shake and Ninja are both designed to be fast, benchmarking them has already improved the speed of Shake.
diff --git a/docs/Why.md b/docs/Why.md
--- a/docs/Why.md
+++ b/docs/Why.md
@@ -7,7 +7,7 @@
 * **Developers running the build system** - Shake based build systems run quickly, require little manual intervention and report estimated completion time as they go.
 * **Developers writing the build system** - Shake provides a powerful language for writing build systems, has excellent support for writing large robust build systems, can express many types of build rules and provides profiling information to help speed up builds.
 
-In the rest of this document we explain and justify the above claims. Shake combines [cutting edge research](http://community.haskell.org/~ndm/downloads/paper-shake_before_building-10_sep_2012.pdf) with a [robust industrial-quality implementation](http://hackage.haskell.org/package/shake/). Shake is in constant use at many large organisations, including [a large investment bank](http://sc.com/), where it was originally developed and has been in use since 2009.
+In the rest of this document we explain and justify the above claims. Shake combines [cutting edge research](http://ndmitchell.com/downloads/paper-shake_before_building-10_sep_2012.pdf) with a [robust industrial-quality implementation](https://hackage.haskell.org/package/shake/). Shake is in constant use at many large organisations, including [a large investment bank](http://sc.com/), where it was originally developed and has been in use since 2009.
 
 #### Expresses many types of build rule
 
@@ -79,9 +79,5 @@
 This document sets out the reasons you _should_ use Shake, but naturally there are some disadvantages:
 
 * Shake build systems are written in Haskell. While that provides many benefits, it does mean the Shake syntax follows that of Haskell, and some Shake errors are reported by the compiler as Haskell type errors. Despite being written in Haskell, the [user manual](Manual.md#readme) introduces Shake assuming no Haskell knowledge, so Haskell knowledge is not a requirement and hopefully should not be a barrier to using Shake.
-* Shake is not likely to be installed by default, while make almost always is.
+* Shake is not likely to be installed by default, while Make almost always is.
 * Shake does not provide default build rules, requiring everything to be expressed in your build system. In practice it seems that the default rules included with make are unsuitable for most large scale projects, but smaller projects may find themselves writing a few additional rules. It is hoped that additional experience with Shake will lead to a library of build rules being developed.
-
-#### Feedback
-
-Do you have any comments on this document? Any questions about Shake? Drop an email to the [mailing list](https://groups.google.com/forum/?fromgroups#!forum/shake-build-system).
diff --git a/docs/shake-progress.png b/docs/shake-progress.png
Binary files a/docs/shake-progress.png and b/docs/shake-progress.png differ
diff --git a/html/profile.html b/html/profile.html
--- a/html/profile.html
+++ b/html/profile.html
@@ -5,18 +5,20 @@
 <meta charset="utf-8" />
 <title>Shake report</title>
 
+<!-- Profiling output -->
+<script src="profile-data.js"></script>
+<script src="version.js"></script>
 <!-- Libraries -->
 <script src="lib/jquery.js"></script>
 <script src="lib/jquery.flot.js"></script>
 <script src="lib/jquery.flot.stack.js"></script>
 <script src="viz.js"></script>
-<!-- Profiling output -->
-<script src="profile-data.js"></script>
-<script src="version.js"></script>
 <!-- Functions for creating info from Shake builds -->
-<script src="shake-util.js"></script>
-<script src="shake-logic.js"></script>
-<script src="shake-ui.js"></script>
+<script src="shake.js"></script>
+<script language="javascript">
+function name(a, b) { return named(a, b); }
+initProfile();
+</script>
 
 <style type="text/css">
 body {font-family: sans-serif; font-size: 10pt;}
@@ -142,9 +144,10 @@
     </p>
     <ul class="space">
         <li><code>group(x : string) : true</code> - assign this item the given group. Multiple calls to <code>group</code> result in a space-separated name. The group name can also be set using <code>name</code> or <code>command</code> with captured regular expressions.</li>
-        <li><code>leaf() : bool</code> - does the rule a leaf, with no dependencies.</li>
+        <li><code>leaf() : bool</code> - is the rule a leaf, with no dependencies.</li>
         <li><code>run() : int</code> - when was the rule last run, with 0 for run in the last tracked build, and higher numbers for run longer ago.</li>
-        <li><code>unchanged() : bool</code> - last time the rule was build, was the value detected as unchanged.</li>
+        <li><code>run(x : int) : bool</code> - equality against a run number, <code>run(0)</code> is equivalent to <code>run() == 0</code>.</li>
+        <li><code>unchanged() : bool</code> - last time the rule was built, did the value not change.</li>
         <li><code>slowestRule() : string</code> - the name of the rule which took longest to build.</li>
         <li><code>name() : string</code> - the name of the current rule.</li>
         <li><code>name(x : RegExp) : bool</code> - a regular expression to match against the name of the current rule. If the regular expression contains captured patterns, these will be used as the <code>group</code></li>
diff --git a/html/progress.html b/html/progress.html
--- a/html/progress.html
+++ b/html/progress.html
@@ -13,7 +13,10 @@
 <script src="progress-data.js"></script>
 <script src="version.js"></script>
 <!-- Functions for creating info from progress info -->
-<script src="shake-progress.js"></script>
+<script src="shake.js"></script>
+<script>
+initProgress();
+</script>
 
 <style type="text/css">
 body {font-family: sans-serif; font-size: 10pt;}
diff --git a/html/shake-logic.js b/html/shake-logic.js
deleted file mode 100644
--- a/html/shake-logic.js
+++ /dev/null
@@ -1,553 +0,0 @@
-/*jsl:option explicit*/
-/*jsl:import shake-util.js*/
-"use strict";
-
-// Data
-//     {name :: String
-//     ,built :: Int
-//     ,changed :: Int
-//     ,depends :: [Int]
-//     ,execution :: Double
-//     ,traces :: [Trace]
-//     }
-//
-// Trace
-//     {start :: Double
-//     ,stop :: Double
-//     ,command :: String
-//     }
-
-
-function bools(x,y)
-{
-    return x === "" ? y : x === y ? x : "both";
-}
-
-function colorAnd(c1, c2)
-{
-    return c1 === null ? c2 : c1 === c2 ? c1 : undefined;
-}
-
-//////////////////////////////////////////////////////////////////////
-// SUMMARY
-
-function /* export */ summary(dat) // :: Data -> Summary
-{
-    // Summary statistics
-    var res =
-        {count : 0 // :: Int, number of rules run
-        ,countLast : 0 // :: Int, number of rules run in the last run
-        ,highestRun : 0 // :: Int, highest run you have seen (add 1 to get the count of runs)
-        ,sumExecution : 0 // :: Seconds, build time in total
-        ,maxExecution : 0 // :: Seconds, longest build rule
-        ,maxExecutionName : "" // :: String, longest build rule
-        ,countTrace : 0, countTraceLast : 0 // :: Int, traced commands run
-        ,sumTrace : 0, sumTraceLast : 0 // :: Seconds, time running traced commands
-        ,maxTrace : 0 // :: Seconds, longest traced command
-        ,maxTraceName : "" // :: String, longest trace command
-        ,maxTraceStopLast : 0 // :: Seconds, time the last traced command stopped
-        };
-
-    // Fold over dat to produce the summary
-    res.count = dat.length;
-    for (var i = 0; i < dat.length; i++)
-    {
-        var isLast = dat[i].built === 0;
-        res.countLast += isLast ? 1 : 0;
-        res.sumExecution += dat[i].execution;
-        res.maxExecution = Math.max(res.maxExecution, dat[i].execution);
-        if (res.maxExecution === dat[i].execution) res.maxExecutionName = dat[i].name;
-        res.highestRun = Math.max(res.highestRun, dat[i].changed); // changed is always greater or equal to built
-        var traces = dat[i].traces;
-        if (!traces) continue;
-        for (var j = 0; j < traces.length; j++)
-        {
-            var time = traces[j].stop - traces[j].start;
-            res.countTrace += 1;
-            res.countTraceLast += isLast ? 1 : 0;
-            res.sumTrace += time;
-            res.sumTraceLast += isLast ? time : 0;
-            res.maxTrace = Math.max(res.maxTrace, time);
-            if (res.maxTrace == time) res.maxTraceName = traces[j].command;
-            res.maxTraceStopLast = Math.max(res.maxTraceStopLast, isLast ? traces[j].stop : 0);
-        }
-    }
-    return res;
-}
-
-function /* export */ showSummary(sum) // Summary -> [String]
-{
-    return ["This database has tracked " + (sum.highestRun+1) + " run" + plural(sum.highestRun+1) + "."
-           ,"There are " + sum.count + " rules (" + sum.countLast + " rebuilt in the last run)."
-           ,"Building required " + sum.countTrace + " traced commands (" + sum.countTraceLast + " in the last run)."
-           ,"The total (unparallelised) build time is " + showTime(sum.sumExecution) + " of which " + showTime(sum.sumTrace) + " is traced commands."
-           ,"The longest rule takes " + showTime(sum.maxExecution) + " (" + sum.maxExecutionName + ") and the longest traced command takes " + showTime(sum.maxTrace) + " (" + sum.maxTraceName + ")."
-           ,"Last run gave an average parallelism of " + (sum.maxTraceStopLast === 0 ? 0 : sum.sumTraceLast / sum.maxTraceStopLast).toFixed(2) + " times over " + showTime(sum.maxTraceStopLast) + "."
-           ];
-}
-
-
-/////////////////////////////////////////////////////////////////////
-// PREPARATION
-
-// Mutate the input data, adding in rdeps, being the 1-level reverse dependencies
-function addRdeps(dat) // Data -> Mutate Data{+rdeps}
-{
-    // find the reverse dependencies
-    var rdeps = [];
-    for (var i = 0; i < dat.length; i++)
-        rdeps[i] = {};
-    for (var i = 0; i < dat.length; i++)
-    {
-        var deps = dat[i].depends;
-        for (var j = 0, n = deps.length; j < n; j++)
-            rdeps[deps[j]][i] = true;
-    }
-    for (var i = 0; i < rdeps.length; i++)
-    {
-        var ans = [];
-        for (var j in rdeps[i])
-            ans.push(Number(j));
-        dat[i].rdeps = ans;
-    }
-}
-
-
-// Given an array of indices, calculate the cost to rebuild if all of them change
-// You must call addRdeps and addCost first
-function calcRebuildCosts(dat,xs) // Data{+rdeps+cost} -> [Int] -> Double
-{
-    var seen = {};
-    var tot = 0;
-    function f(i)
-    {
-        if (seen[i]) return;
-        seen[i] = true;
-        tot += dat[i].execution;
-        var deps = dat[i].rdeps;
-        for (var j = 0, n = deps.length; j < n; j++)
-            f(deps[j]);
-    }
-    if (xs.length === 1 && dat[xs[0]].depends.length === 1)
-        tot = dat[dat[xs[0]].depends[0]].cost + dat[xs[0]].execution;
-    else
-    {
-        for (var i = 0, n = xs.length; i < n; i++)
-            f(xs[i]);
-    }
-    return tot;
-}
-
-
-// Mutate the dat data, adding in cost, being the cost to rebuild if this item changes
-function addCost(dat) // Data -> Mutate Data{+cost}
-{
-    for (var i = 0; i < dat.length; i++)
-        dat[i].cost = calcRebuildCosts(dat, [i]);
-}
-
-function prepare(sum, dat) // Data -> DataEx
-{
-    addRdeps(dat);
-    addCost(dat);
-
-    function toHash(r){return typeof r === "string" ? "$" + r : "/" + r.source;}
-
-    function findDirect(key)
-    {
-        var c = cache(toHash, function(r){
-            var want = {};
-            for (var i = 0; i < dat.length; i++)
-            {
-                if (testRegExp(r, dat[i].name))
-                {
-                    var deps = dat[i][key];
-                    for (var j = 0; j < deps.length; j++)
-                        want[deps[j]] = true;
-                }
-            }
-            return want;
-        });
-        return function(i,r)
-        {
-            if (i in c(r))
-                return true;
-            else
-                return false;
-        };
-    }
-
-    function findTransitive(key, dirFwd)
-    {
-        var c = cache(toHash, function(r){
-            var want = {};
-            for (var i = 0; i < dat.length; i++)
-            {
-                var j = dirFwd ? i : dat.length - 1 - i;
-                if ((j in want) || testRegExp(r,dat[j].name))
-                {
-                    want[j] = true;
-                    var deps = dat[j][key];
-                    for (var k = 0; k < deps.length; k++)
-                        want[deps[k]] = true;
-                }
-            }
-            return want;
-        });
-        return function(i,r){return i in c(r);};
-    }
-
-    return {original: dat
-           ,summary: sum
-           ,dependsOnThis: findDirect("rdeps")
-           ,thisDependsOn: findDirect("depends")
-           ,dependsOnThisTransitive: findTransitive("depends", false)
-           ,thisDependsOnTransitive: findTransitive("rdeps", true)
-           };
-}
-
-
-/////////////////////////////////////////////////////////////////////
-// RULES
-
-function ruleFilter(dat, query) // DataEx -> Query -> Dict String {items: [DataIndex], color: color}
-{
-    queryData = dat;
-    var f = readQuery(query);
-    var res = {};
-
-    for (queryKey = 0; queryKey < dat.original.length; queryKey++)
-    {
-        queryVal = dat.original[queryKey];
-        queryName = queryVal.name;
-        queryGroup = null;
-        queryBackColor = null;
-        queryTextColor = null;
-        if (f())
-        {
-            if (queryGroup === null) queryGroup = queryName;
-            if (!(queryGroup in res))
-                res[queryGroup] = {items: [queryKey], text: queryTextColor, back: queryBackColor};
-            else
-            {
-                var c = res[queryGroup];
-                c.items.push(queryKey);
-                c.text = colorAnd(c.text, queryTextColor);
-                c.back = colorAnd(c.back, queryBackColor);
-            }
-        }
-    }
-    return res;
-}
-
-function ruleTable(dat, query) // DataEx -> Query -> [Record]
-{
-    var res = ruleFilter(dat, query);
-    var ans = [];
-    for (var s in res)
-    {
-        var xs = res[s].items;
-        var time = 0;
-        var leaf = "";
-        var unchanged = "";
-        var run = 100000;
-        for (var i = 0; i < xs.length; i++)
-        {
-            var x = dat.original[xs[i]];
-            time += x.execution;
-            leaf = bools(leaf, x.depends.length === 0);
-            unchanged = bools(unchanged, x.changed !== x.built);
-            run = Math.min(run,x.built);
-        }
-        ans.push({name:s, count:xs.length, time:time, back:res[s].back, text:res[s].text, cost:calcRebuildCosts(dat.original,xs), leaf:leaf, run:run, unchanged:unchanged});
-    }
-    return ans;
-}
-
-function ruleGraph(dat, query) // DataEx -> Query -> [Record]
-{
-    var res = ruleFilter(dat, query);
-
-    var map = {}; // :: Dict Int [Int] -- which nodes a node lives at
-
-    // loop through each value in res, putting it into map (these are parents)
-    // for any not present, descend through the dat.original list, if you aren't included, add, if you are included, skip
-    var direct = {};
-    var ind = -1;
-    for (var s in res)
-    {
-        ind++;
-        var xs = res[s].items;
-        for (var i = 0; i < xs.length; i++)
-            direct[xs[i]] = ind;
-    }
-    function getDirect(key)
-    {
-        return key in direct ? [direct[key]] : [];
-    }
-
-    var indirect = {};
-    function getIndirect(key)
-    {
-        if (key in indirect) return indirect[key];
-        if (key in direct) return [];
-        var ds = dat.original[key].depends;
-        var res = [];
-        for (var j = 0; j < ds.length; j++)
-        {
-            res.push(getIndirect(ds[j]));
-            res.push(getDirect(ds[j]));
-        }
-        res = concatNub(res);
-        indirect[key] = res;
-        return res;
-    }
-
-    var ans = [];
-    for (var s in res)
-    {
-        var xs = res[s].items;
-        var ds = [];
-        var is = [];
-        for (var i = 0; i < xs.length; i++)
-        {
-            var depends = dat.original[xs[i]].depends;
-            for (var j = 0; j < depends.length; j++)
-            {
-                ds.push(getDirect(depends[j]));
-                is.push(getIndirect(depends[j]));
-            }
-        }
-        ds = concatNub(ds);
-        is = concatNub(is);
-        ans.push({name:s, text:res[s].text, back:res[s].back, parents:ds, ancestors:is});
-    }
-    return ans;
-}
-
-
-/////////////////////////////////////////////////////////////////////
-// COMMANDS
-
-function commandFilter(last, dat, query) // DataEx -> Query -> Dict String [Trace]
-{
-    queryData = dat;
-    var f = readQuery(query);
-    var res = {};
-
-    for (queryKey = 0; queryKey < dat.original.length; queryKey++)
-    {
-        queryVal = dat.original[queryKey];
-        if (last && queryVal.built !== 0) continue;
-
-        var val = {};
-        for (var s in queryVal)
-            val[s] = queryVal[s];
-        var ts = queryVal.traces || [];
-        queryVal = val;
-        queryName = queryVal.name;
-        queryBackColor = null;
-        queryTextColor = null;
-        for (var i = 0; i < ts.length; i++)
-        {
-            queryVal.traces = [ts[i]];
-            queryGroup = null;
-            if (f())
-            {
-                if (queryGroup === null) queryGroup = ts[i].command;
-                if (!(queryGroup in res))
-                    res[queryGroup] = {items: [ts[i]], text:queryTextColor, back:queryBackColor};
-                else
-                {
-                    var c = res[queryGroup];
-                    c.items.push(ts[i]);
-                    c.text = colorAnd(c.text, queryTextColor);
-                    c.back = colorAnd(c.back, queryBackColor);
-                }
-            }
-        }
-    }
-    return res;
-}
-
-function commandTable(dat, query) // DataEx -> Query -> [Record]
-{
-    var res = commandFilter(false, dat, query);
-    var ans = [];
-    for (var s in res)
-    {
-        var xs = res[s].items;
-        var time = 0;
-        for (var i = 0; i < xs.length; i++)
-            time += xs[i].stop - xs[i].start;
-        ans.push({name:s, count:xs.length, text:res[s].text, back:res[s].back, time:time});
-    }
-    return ans;
-}
-
-function commandPlot(dat, query, buckets) // DataEx -> Query -> Int -> Dict String [Double]
-{
-    var end = dat.summary.maxTraceStopLast;
-    var res = commandFilter(true, dat, query);
-    var ans = {};
-    for (var s in res)
-    {
-        var ts = res[s].items;
-        var xs = [];
-        for (var i = 0; i <= buckets; i++)
-            xs.push(0); // fill with 1 more element, but the last bucket will always be 0
-
-        for (var i = 0; i < ts.length; i++)
-        {
-            var start = ts[i].start * buckets / end;
-            var stop = ts[i].stop * buckets / end;
-
-            if (Math.floor(start) === Math.floor(stop))
-                xs[Math.floor(start)] += stop - start;
-            else
-            {
-                for (var j = Math.ceil(start); j < Math.floor(stop); j++)
-                    xs[j]++;
-                xs[Math.floor(start)] += Math.ceil(start) - start;
-                xs[Math.floor(stop)] += stop - Math.floor(stop);
-            }
-        }
-        ans[s] = {items: xs.slice(0,buckets), back: res[s].back || null};
-    }
-    return ans;
-}
-
-
-/////////////////////////////////////////////////////////////////////
-// ENVIRONMENT
-
-function readQuery(query)
-{
-    var f;
-    try {
-        f = new Function("return " + (query === "" ? "true" : query));
-    } catch (e) {
-        throw {user:true, name:"parse", query:query, message:e.toString()};
-    }
-    return function(){
-        try {
-            return f();
-        } catch (e) {
-            throw {user:true, name:"execution", query:query, message:e.toString()};
-        }
-    };
-}
-
-
-// These are global variables mutated/queried by query execution
-var queryData = {};
-var queryKey = 0;
-var queryVal = {};
-var queryName = "";
-var queryGroup = null;
-var queryBackColor = null;
-var queryTextColor = null;
-
-function childOf(r){return queryData.dependsOnThis(queryKey, r);}
-function parentOf(r){return queryData.thisDependsOn(queryKey, r);}
-function ancestorOf(r){return queryData.dependsOnThisTransitive(queryKey, r);}
-function descendantOf(r){return queryData.thisDependsOnTransitive(queryKey, r);}
-function descendentOf(r){return descendantOf(r);}
-
-function /* export */ group(x)
-{
-    if (queryGroup === null) queryGroup = "";
-    queryGroup += (queryGroup === "" ? "" : " ") + x;
-    return true;
-}
-
-function backColor(c, b)
-{
-    if (b === undefined || b)
-        queryBackColor = c;
-    return true;
-}
-
-function textColor(c, b)
-{
-    if (b === undefined || b)
-        queryTextColor = c;
-    return true;
-}
-
-function rename(from, to)
-{
-    queryName = queryName.replace(from, to || "");
-    return true;
-}
-
-function slowestRule()
-{
-    return queryData.summary.maxExecutionName;
-}
-
-function /* export */ leaf()
-{
-    return queryVal.depends.length === 0;
-}
-
-function /* export */ run()
-{
-    return queryVal.built;
-}
-
-function /* export */ unchanged()
-{
-    return queryVal.changed !== queryVal.built;
-}
-
-function /* export */ name(r, groupName)
-{
-    if (r === undefined)
-        return queryName;
-
-    var res = execRegExp(r, queryName);
-    if (res === null)
-    {
-        if (groupName === undefined)
-            return false;
-        else
-        {
-            group(groupName);
-            return true;
-        }
-    }
-    if (res.length !== 1)
-    {
-        for (var i = 1; i < res.length; i++)
-            group(res[i]);
-    }
-    return true;
-}
-
-function /* export */ command(r, groupName)
-{
-    var n = (queryVal.traces || []).length;
-    if (r === undefined)
-        return n === 0 ? "" : queryVal.traces[0].command;
-
-    for (var i = 0; i < n; i++)
-    {
-        var res = execRegExp(r, queryVal.traces[i].command);
-        if (res === null)
-            continue;
-        if (res.length !== 1)
-        {
-            for (var j = 1; j < res.length; j++)
-                group(res[j]);
-        }
-        return true;
-    }
-    if (groupName === undefined)
-        return false;
-    else
-    {
-        group(groupName);
-        return true;
-    }
-}
diff --git a/html/shake-progress.js b/html/shake-progress.js
deleted file mode 100644
--- a/html/shake-progress.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*jsl:option explicit*/
-"use strict";
-
-// Data
-//     {name :: String
-//     ,values :: [Progress]
-//     }
-//
-// Progress
-//     {idealSecs :: Double
-//     ,idealPerc :: Double
-//     ,actualSecs :: Double
-//     ,actualPerc :: Double
-//     }
-
-$(function(){
-    $(".version").html("Generated by <a href='http://shakebuild.com'>Shake " + version + "</a>.");
-    $("#output").html("");
-    for (var i = 0; i < shake.length; i++)
-    {
-        var x = shake[i];
-        var actual = [];
-        var ideal = [];
-        // Start at t = 5 seconds, since the early progress jumps a lot
-        for (var t = 5; t < x.values.length; t++)
-        {
-            var y = x.values[t];
-            actual.push([y.idealSecs, y.actualSecs]);
-            ideal.push([y.idealSecs, y.idealSecs]);
-        }
-        var ys = [{data:ideal, color:"gray"}, {label:x.name, data:actual, color:"red"}];
-        var div = $("<div class='plot'>");
-        $("#output").append(div);
-        $.plot(div, ys, {
-            xaxis: {
-                transform: function (v) { return -v; },
-                inverseTransform: function (v) { return -v; }
-            }
-        });
-    }
-})
diff --git a/html/shake-ui.js b/html/shake-ui.js
deleted file mode 100644
--- a/html/shake-ui.js
+++ /dev/null
@@ -1,346 +0,0 @@
-/*jsl:option explicit*/
-/*jsl:import shake-logic.js*/
-"use strict";
-
-// Report
-//     {mode :: String
-//     ,query :: String
-//     ,sort :: String
-//     ,sortRev :: Bool
-//     }
-//
-
-var defaultMode = "summary";
-var defaultQuery = "";
-var defaultSort = "time";
-
-var currentTable = null;
-var currentPlot = null;
-
-
-/////////////////////////////////////////////////////////////////////
-// GLOBAL DATA
-
-var shakeSummary = summary(shake);
-var shakeEx = prepare(shakeSummary, shake);
-
-var report = {}; // :: Report
-
-function reportURL(report) // :: Report -> URL
-{
-    return "?mode=" + report.mode +
-           (report.query === defaultQuery ? "" : "&query=" + escape(report.query).replace(/\+/g,"%2B")) +
-           (report.sort === undefined || (!report.sortRev && report.sort === defaultSort) ? "" :
-               "&sort=" + (report.sortRev ? "!" : "") + report.sort);
-}
-
-function urlReport() // :: IO Report
-{
-    var params = $.getParameters();
-    var sort = params.sort || defaultSort;
-    var sortRev = false;
-    if (sort.substr(0,1) == "!")
-    {
-        sort = sort.substr(1);
-        sortRev = true;
-    }
-    return {mode: params.mode || defaultMode
-           ,query: params.query || defaultQuery
-           ,sort: sort
-           ,sortRev: sortRev
-           };
-}
-
-function enteredReport()
-{
-    return {mode: $("#mode").val()
-           ,query: $("#query").val()
-           ,sort: report.sort
-           ,sortRev: report.sortRev
-           };
-}
-
-function setReport(r, replace, run)
-{
-    var changed = false;
-    var report2 = recordUnion(r, report);
-    $("#mode").val(report2.mode);
-    $("#query").val(report2.query);
-    $("#run").enable(false).attr("title", "The current query is displayed");
-    if (recordEq(report,report2)) return;
-    report = report2;
-
-    if (window.history)
-    {
-        var title = report.mode + (report.query === "" ? "" : ": " + report.query);
-        var url = reportURL(report);
-        if (replace)
-            window.history.replaceState(report, title, url);
-        else
-            window.history.pushState(report, title, url);
-    }
-    $("#link").attr("href", reportURL(report));
-
-    if (run)
-        runReport();
-}
-
-
-/////////////////////////////////////////////////////////////////////
-// TABLE SHOWING
-
-var rightAlign = {count:null, time:null, cost:null, run:null, leaf:null, unchanged:null};
-var twoColumns = {cost:null, time:null};
-var defaultRevSort = {run:null, name:null};
-
-function tableSort(x)
-{
-    if (report.sort === x)
-        setReport({sortRev: !report.sortRev}, true, false);
-    else
-        setReport({sort: x, sortRev: x in defaultRevSort}, true, false);
-    showTable(currentTable);
-}
-
-function showTable(xs)
-{
-    currentTable = xs;
-    if (xs.length === 0)
-    {
-        $("#output").html("No data found");
-        return;
-    }
-    if (!(report.sort in xs[0]))
-        setReport({sort:defaultSort, sortRev:false}, true, false);
-
-    xs.sort(function(a,b){return (report.sortRev ? -1 : 1) * (b[report.sort] > a[report.sort] ? 1 : -1);});
-
-    var res = "<table class='data'><tr class='header'>";
-    for (var s in xs[0])
-    {
-        if (s === "back" || s === "text") continue;
-        res += s in twoColumns ? "<td colspan='2' style='text-align:center;'" :
-               s in rightAlign ? "<td style='text-align:right;'" :
-               "<td";
-        res += " onclick=\"tableSort('" + s + "')\">" + s;
-        if (s === report.sort)
-            res += " <span class='sort'>" + (report.sortRev ? "&#9650;" : "&#9660;") + "</span>";
-        res += "</td>";
-    }
-    res += "</tr>";
-
-    for (var i = 0; i < xs.length; i++)
-    {
-        var x = xs[i];
-        res += "<tr";
-        if (x.back) res += " style='background-color:" + x.back + ";'";
-        if (x.text) res += " style='color:" + x.text + ";'";
-        res += ">";
-        for (var s in xs[0])
-        {
-            if (s === "text" || s === "back") continue;
-            res += "<td" + (s in rightAlign ? " style='text-align:right;'" : "") + ">";
-            if (s === "count")
-                res += x[s] + " &times;";
-            else if (s === "time" || s === "cost")
-                res += showTime(x[s]) + "</td><td style='text-align:right;'>" + showPerc(x[s] / shakeSummary.sumExecution);
-            else
-                res += x[s];
-            res += "</td>";
-        }
-        res += "</tr>";
-    }
-    res += "</table>";
-
-    $("#output").html(res);
-}
-
-
-/////////////////////////////////////////////////////////////////////
-// SHOW A PLOT
-
-function showPlot(series, options)
-{
-    var $output = $("#output");
-    var width = $output.width();
-    var height = $output.height();
-
-    if (series === null && options === null)
-    {
-        if (width === currentPlot.width && height === currentPlot.height)
-            return;
-        series = currentPlot.series;
-        options = currentPlot.options;
-    }
-    currentPlot = {series:series, options:options, width:width, height:height};
-
-    // Fudge factors to get it displaying nicely, seems Flot goes outside its bounds
-    var div = $("<div>").width(width - 20).height(height - 10);
-    $("#output").html("").append(div);
-    $.plot(div, series, options);
-}
-
-window.onresize = function(){
-    if (currentPlot !== null)
-        showPlot(null, null);
-};
-
-/////////////////////////////////////////////////////////////////////
-// RUNNING
-
-function runReport()
-{
-    currentTable = null;
-    currentPlot = null;
-
-    try
-    {
-        switch(report.mode)
-        {
-        case "summary":
-            var res = showSummary(shakeSummary);
-            var s = $("#welcome").html();
-            s += "<ul>";
-            for (var i = 0; i < res.length; i++)
-                s += "<li>" + res[i] + "</li>";
-            s += "</ul>";
-            s += "<p class='version'>Generated by <a href='http://shakebuild.com'>Shake " + version + "</a>.</p>";
-            $("#output").html(s);
-            break;
-
-        case "cmd-plot":
-            var xs = commandPlot(shakeEx, report.query, 100);
-            var ys = [];
-            for (var s in xs)
-            {
-                var x = xs[s].items;
-                var data = [];
-                for (var j = 0; j < x.length; j++)
-                    data.push([j, x[j]]);
-                ys.push({label:s, values:x, data:data, color:xs[s].back, avg:sum(x) / x.length});
-            }
-            if (ys.length === 0)
-            {
-                $("#output").html("No data found, " +
-                    (shakeEx.summary.countTraceLast === 0
-                        ? "there were no traced commands in the last run."
-                        : "perhaps your filter is too restrictive?"));
-            }
-            else
-            {
-                ys.sort(function(a,b){return a.avg - b.avg;});
-                showPlot(ys, {
-                    legend: {show:true, position:"nw", sorted:"reverse"},
-                    series: {stack:true, lines:{lineWidth:0,fill:1}},
-                    yaxis: {min:0},
-                    xaxis: {tickFormatter: function (i){return showTime(shakeSummary.maxTraceStopLast * i / 100);}}
-                });
-            }
-            break;
-
-        case "cmd-table":
-            showTable(commandTable(shakeEx, report.query));
-            break;
-
-        case "rule-table":
-            showTable(ruleTable(shakeEx, report.query));
-            break;
-
-        case "rule-graph":
-            var xs = ruleGraph(shakeEx, report.query);
-            if (xs.length > 250)
-                $("#output").html("Viewing a graph with > 250 nodes is not supported, and you have " + xs.length + " nodes. Try grouping more aggressively");
-            else if (typeof Viz === 'undefined')
-                $("#output").html("Profile reports do not seem to have been built with GraphViz support, this feature is unavailable.");
-            else
-            {
-                var res = "digraph \"\"{";
-                res += "graph[nodesep=0.15,ranksep=0.3];";
-                res += "node[fontname=\"sans-serif\",fontsize=9,penwidth=0.5,height=0,width=0];";
-                res += "edge[penwidth=0.5,arrowsize=0.5];";
-                for (var i = 0; i < xs.length; i++)
-                {
-                    res += "a" + i + "[label=\"" + xs[i].name.split("\\").join("\\\\").split("\"").join("\\\"") + "\"";
-                    if (xs[i].back) res += ",style=filled,color=\"" + xs[i].back + "\"";
-                    if (xs[i].text) res += ",fontcolor=\"" + xs[i].text + "\"";
-                    res += "];";
-                    var parents = xs[i].parents;
-                    for (var j = 0; j < parents.length; j++)
-                        res += "a" + i + "->a" + parents[j] + ";";
-                    var ancestors = xs[i].ancestors;
-                    for (var j = 0; j < ancestors.length; j++)
-                        res += "a" + i + "->a" + ancestors[j] + "[style=dashed];";
-                }
-                res += "}";
-                $("#output").html(Viz(res,"svg"));
-            }
-            break;
-
-        case "help":
-            $("#output").html($("#help").html());
-            break;
-
-        default:
-            throw "Unknown report type: " + report.mode;
-            break;
-        }
-    }
-    catch (e)
-    {
-        if (!(e && e.user))
-            throw e;
-        $("#output").html($("#error").html());
-        for (var s in e)
-            $("#output ." + s).text(e[s]);
-    }
-}
-
-
-/////////////////////////////////////////////////////////////////////
-// STATE NAVIGATION
-
-function example(mode,query)
-{
-    setReport({mode:mode, query:query, sortRev:false, sort:defaultSort}, false, true);
-    return false;
-}
-
-$(function(){
-    setReport(urlReport(), true, true);
-
-    $("#mode,#query").bind("input change",function(){
-        var mode = $("#mode").val();
-        var query = $("#query").val();
-        var enable = mode !== report.mode || query !== report.query;
-        $("#run").enable(enable).attr("title", enable ? "" : "The current query is displayed");
-        $("#link").attr("href", reportURL(enteredReport()));
-    });
-
-    $("#run").click(function(){
-        setReport(enteredReport(), false, true);
-    });
-
-    window.onpopstate = function (e){
-        setReport(urlReport(), true, true);
-    };
-});
-
-/////////////////////////////////////////////////////////////////////
-// TEMPLATES
-
-$(function(){
-    $("a.example").each(function(){
-        var mode = $(this).attr("data-mode");
-        var query = $(this).attr("data-query");
-        if (query === undefined) query = $(this).text();
-        var href = reportURL({mode:mode, query:query});
-        var onclick = "return example(unescape('" + escape(mode) + "'),unescape('" + escape(query) + "'));";
-        $(this).attr("href", href).attr("target","_blank")[0].setAttribute("onclick",onclick);
-    });
-
-    $("a.shake").each(function(){
-        var href = "http://hackage.haskell.org/packages/archive/shake/latest/doc/html/Development-Shake.html#v:" +
-                   $(this).text().replace("'","-39-");
-        $(this).attr("href", href).attr("target","_blank");
-    });
-});
diff --git a/html/shake-util.js b/html/shake-util.js
deleted file mode 100644
--- a/html/shake-util.js
+++ /dev/null
@@ -1,167 +0,0 @@
-/*jsl:option explicit*/
-"use strict";
-
-/////////////////////////////////////////////////////////////////////
-// JQUERY EXTENSIONS
-
-jQuery.fn.enable = function (x)
-{
-    // Set the values to enabled/disabled
-    return this.each(function () {
-        if (x)
-            $(this).removeAttr('disabled');
-        else
-            $(this).attr('disabled','disabled');
-    });
-};
-
-jQuery.getParameters = function()
-{
-    // From http://stackoverflow.com/questions/901115/get-querystring-values-with-jquery/3867610#3867610
-    var params = {};
-    var a = /\+/g;  // Regex for replacing addition symbol with a space
-    var r = /([^&=]+)=?([^&]*)/g;
-    var d = function (s) { return decodeURIComponent(s.replace(a, " ")); };
-    var q = window.location.search.substring(1);
-
-    while (true)
-    {
-        var e = r.exec(q);
-        if (!e) break;
-        params[d(e[1])] = d(e[2]);
-    }
-    return params;
-};
-
-
-/////////////////////////////////////////////////////////////////////
-// STRING FORMATTING
-
-function /* export */ showTime(x) // :: Double -> String
-{
-    function digits(x){var s = String(x); return s.length === 1 ? "0" + s : s;}
-
-    if (x >= 3600)
-    {
-        x = Math.round(x / 60);
-        return Math.floor(x / 60) + "h" + digits(x % 60) + "m";
-    }
-    else if (x >= 60)
-    {
-        x = Math.round(x);
-        return Math.floor(x / 60) + "m" + digits(x % 60) + "s";
-    }
-    else
-        return x.toFixed(2) + "s";
-}
-
-function /* export */ showPerc(x) // :: Double -> String
-{
-    return (x*100).toFixed(2) + "%";
-}
-
-function /* export */ plural(n,not1,is1) // :: Int -> Maybe String -> Maybe String -> String
-{
-    return n === 1
-        ? (is1 === undefined ? "" : is1)
-        : (not1 === undefined ? "s" : not1);
-}
-
-
-/////////////////////////////////////////////////////////////////////
-// MISC
-
-function sum(xs) // :: Num a => [a] -> a
-{
-    var res = 0;
-    for (var i = 0; i < xs.length; i++)
-        res += xs[i];
-    return res;
-}
-
-function testRegExp(r, s)
-{
-    if (typeof r === "string")
-        return s.indexOf(r) !== -1;
-    else
-        return r.test(s);
-}
-
-function execRegExp(r, s)
-{
-    if (typeof r === "string")
-        return s.indexOf(r) === -1 ? null : [];
-    else
-        return r.exec(s);
-}
-
-function listEq(xs, ys) // :: Eq a => [a] -> [a] -> Bool
-{
-    if (xs.length !== ys.length) return false;
-    for (var i = 0; i < xs.length; i++)
-    {
-        if (xs[i] !== ys[i])
-            return false;
-    }
-    return true;
-}
-
-function cache(str, f) // :: (k -> String) -> (k -> v) -> (k -> v)
-{
-    var cache = {};
-    return function(k){
-        var s = str(k);
-        if (!(s in cache))
-            cache[s] = f(k);
-        return cache[s];
-    };
-}
-
-function recordEq(xs, ys) // :: Record -> Record -> Bool
-{
-    function f(a,b)
-    {
-        for (var s in a)
-        {
-            if (a[s] !== b[s]) return false;
-        }
-        return true;
-    }
-    return f(xs,ys) && f(ys,xs);
-}
-
-function recordCopy(xs) // :: Record -> Record
-{
-    var res = {};
-    for (var s in xs)
-        res[s] = xs[s];
-    return res;
-}
-
-function recordUnion(xs,ys) // :: Record -> Record -> Record -- left biased
-{
-    var res = recordCopy(ys);
-    for (var s in xs)
-        res[s] = xs[s];
-    return res;
-}
-
-function concatNub(xs) // :: Eq a => [[a]] -> [a]
-{
-    var res = [];
-    var seen = {};
-    for (var i = 0; i < xs.length; i++)
-    {
-        var x = xs[i];
-        for (var j = 0; j < x.length; j++)
-        {
-            var e = x[j];
-            if (!(e in seen))
-            {
-                seen[e] = null;
-                res.push(e);
-            }
-        }
-    }
-    return res;
-}
diff --git a/html/shake.js b/html/shake.js
new file mode 100644
--- /dev/null
+++ b/html/shake.js
@@ -0,0 +1,978 @@
+"use strict";
+var Summary = (function () {
+    function Summary() {
+        this.count = 0;
+        this.countLast = 0;
+        this.highestRun = 0;
+        this.sumExecution = 0;
+        this.maxExecution = 0;
+        this.maxExecutionName = "";
+        this.countTrace = 0;
+        this.countTraceLast = 0;
+        this.sumTrace = 0;
+        this.sumTraceLast = 0;
+        this.maxTrace = 0;
+        this.maxTraceName = "";
+        this.maxTraceStopLast = 0;
+    }
+    return Summary;
+})();
+function summary(dat) {
+    var res = new Summary();
+    res.count = dat.length;
+    for (var _i = 0; _i < dat.length; _i++) {
+        var e = dat[_i];
+        var isLast = e.built === 0;
+        res.countLast += isLast ? 1 : 0;
+        res.sumExecution += e.execution;
+        res.maxExecution = Math.max(res.maxExecution, e.execution);
+        if (res.maxExecution === e.execution)
+            res.maxExecutionName = e.name;
+        res.highestRun = Math.max(res.highestRun, e.changed);
+        var traces = e.traces;
+        if (!traces)
+            continue;
+        for (var _a = 0; _a < traces.length; _a++) {
+            var t_1 = traces[_a];
+            var time = t_1.stop - t_1.start;
+            res.countTrace += 1;
+            res.countTraceLast += isLast ? 1 : 0;
+            res.sumTrace += time;
+            res.sumTraceLast += isLast ? time : 0;
+            res.maxTrace = Math.max(res.maxTrace, time);
+            if (res.maxTrace == time)
+                res.maxTraceName = t_1.command;
+            res.maxTraceStopLast = Math.max(res.maxTraceStopLast, isLast ? t_1.stop : 0);
+        }
+    }
+    return res;
+}
+function showSummary(sum) {
+    return ["This database has tracked " + (sum.highestRun + 1) + " run" + plural(sum.highestRun + 1) + ".",
+        "There are " + sum.count + " rules (" + sum.countLast + " rebuilt in the last run).",
+        "Building required " + sum.countTrace + " traced commands (" + sum.countTraceLast + " in the last run).",
+        "The total (unparallelised) build time is " + showTime(sum.sumExecution) + " of which " + showTime(sum.sumTrace) + " is traced commands.",
+        "The longest rule takes " + showTime(sum.maxExecution) + " (" + sum.maxExecutionName + ") and the longest traced command takes " + showTime(sum.maxTrace) + " (" + sum.maxTraceName + ").",
+        "Last run gave an average parallelism of " + (sum.maxTraceStopLast === 0 ? 0 : sum.sumTraceLast / sum.maxTraceStopLast).toFixed(2) + " times over " + showTime(sum.maxTraceStopLast) + "."
+    ];
+}
+var Prepare = (function () {
+    function Prepare() {
+    }
+    return Prepare;
+})();
+function addRdeps(dat) {
+    var rdeps = [];
+    for (var i = 0; i < dat.length; i++)
+        rdeps[i] = {};
+    for (var i = 0; i < dat.length; i++) {
+        for (var _i = 0, _a = dat[i].depends; _i < _a.length; _i++) {
+            var j = _a[_i];
+            rdeps[j][i] = null;
+        }
+    }
+    var res = dat;
+    for (var i = 0; i < rdeps.length; i++) {
+        var ans = [];
+        for (var j in rdeps[i])
+            ans.push(Number(j));
+        res[i].rdeps = ans;
+    }
+    return res;
+}
+function calcRebuildCosts(dat, xs) {
+    var seen = {};
+    var tot = 0;
+    function f(i) {
+        if (i in seen)
+            return;
+        seen[i] = null;
+        tot += dat[i].execution;
+        for (var _i = 0, _a = dat[i].rdeps; _i < _a.length; _i++) {
+            var j = _a[_i];
+            f(j);
+        }
+    }
+    if (xs.length === 1 && dat[xs[0]].depends.length === 1)
+        tot = dat[dat[xs[0]].depends[0]].cost + dat[xs[0]].execution;
+    else {
+        for (var _i = 0; _i < xs.length; _i++) {
+            var x = xs[_i];
+            f(x);
+        }
+    }
+    return tot;
+}
+function addCost(dat) {
+    var res = dat;
+    for (var i = 0; i < dat.length; i++) {
+        res[i].cost = calcRebuildCosts(res, [i]);
+    }
+    return res;
+}
+function prepare(dat_) {
+    var sum = summary(dat_);
+    var dat = addCost(addRdeps(dat_));
+    function toHash(r) {
+        return typeof r === "string" ? "$" + r : "/" + r.source;
+    }
+    function findDirect(key) {
+        var c = cache(toHash, function (r) {
+            var want = {};
+            for (var _i = 0; _i < dat.length; _i++) {
+                var e = dat[_i];
+                if (testRegExp(r, e.name)) {
+                    var deps = (e)[key];
+                    for (var _a = 0; _a < deps.length; _a++) {
+                        var j = deps[_a];
+                        want[j] = null;
+                    }
+                }
+            }
+            return want;
+        });
+        return function (i, r) { return i in c(r); };
+    }
+    function findTransitive(key, dirFwd) {
+        var c = cache(toHash, function (r) {
+            var want = {};
+            for (var i = 0; i < dat.length; i++) {
+                var j = dirFwd ? i : dat.length - 1 - i;
+                if ((j in want) || testRegExp(r, dat[j].name)) {
+                    want[j] = null;
+                    var deps = (dat[j])[key];
+                    for (var _i = 0; _i < deps.length; _i++) {
+                        var k = deps[_i];
+                        want[k] = null;
+                    }
+                }
+            }
+            return want;
+        });
+        return function (i, r) { return i in c(r); };
+    }
+    return {
+        original: dat,
+        summary: sum,
+        dependsOnThis: findDirect("rdeps"),
+        thisDependsOn: findDirect("depends"),
+        dependsOnThisTransitive: findTransitive("depends", false),
+        thisDependsOnTransitive: findTransitive("rdeps", true)
+    };
+}
+function colorAnd(c1, c2) {
+    return c1 === null ? c2 : c1 === c2 ? c1 : undefined;
+}
+var Result = (function () {
+    function Result() {
+    }
+    return Result;
+})();
+function ruleFilter(dat, query) {
+    queryData = dat;
+    var f = readQuery(query);
+    var res = {};
+    for (queryKey = 0; queryKey < dat.original.length; queryKey++) {
+        queryVal = dat.original[queryKey];
+        queryName = queryVal.name;
+        queryGroup = null;
+        queryBackColor = null;
+        queryTextColor = null;
+        if (f()) {
+            if (queryGroup === null)
+                queryGroup = queryName;
+            if (!(queryGroup in res))
+                res[queryGroup] = { items: [queryKey], text: queryTextColor, back: queryBackColor };
+            else {
+                var c = res[queryGroup];
+                c.items.push(queryKey);
+                c.text = colorAnd(c.text, queryTextColor);
+                c.back = colorAnd(c.back, queryBackColor);
+            }
+        }
+    }
+    return res;
+}
+var ResultTable = (function () {
+    function ResultTable() {
+    }
+    return ResultTable;
+})();
+function ruleTable(dat, query) {
+    function bools(x, y) {
+        return x === "" ? y : x === y ? x : "both";
+    }
+    var res = ruleFilter(dat, query);
+    var ans = [];
+    for (var s in res) {
+        var xs = res[s].items;
+        var time = 0;
+        var leaf = "";
+        var unchanged = "";
+        var run = 100000;
+        for (var i = 0; i < xs.length; i++) {
+            var x = dat.original[xs[i]];
+            time += x.execution;
+            leaf = bools(leaf, x.depends.length === 0);
+            unchanged = bools(unchanged, x.changed !== x.built);
+            run = Math.min(run, x.built);
+        }
+        ans.push({ name: s, count: xs.length, time: time, back: res[s].back, text: res[s].text, cost: calcRebuildCosts(dat.original, xs), leaf: leaf, run: run, unchanged: unchanged });
+    }
+    return ans;
+}
+var ResultGraph = (function () {
+    function ResultGraph() {
+    }
+    return ResultGraph;
+})();
+function ruleGraph(dat, query) {
+    var res = ruleFilter(dat, query);
+    var map = {};
+    var direct = {};
+    var ind = -1;
+    for (var s in res) {
+        ind++;
+        var xs = res[s].items;
+        for (var i = 0; i < xs.length; i++)
+            direct[xs[i]] = ind;
+    }
+    function getDirect(key) {
+        return key in direct ? [direct[key]] : [];
+    }
+    var indirect = {};
+    function getIndirect(key) {
+        if (key in indirect)
+            return indirect[key];
+        if (key in direct)
+            return [];
+        var ds = dat.original[key].depends;
+        var res = [];
+        for (var j = 0; j < ds.length; j++) {
+            res.push(getIndirect(ds[j]));
+            res.push(getDirect(ds[j]));
+        }
+        var res2 = concatNub(res);
+        indirect[key] = res2;
+        return res2;
+    }
+    var ans = [];
+    for (var s in res) {
+        var xs = res[s].items;
+        var ds = [];
+        var is = [];
+        for (var i = 0; i < xs.length; i++) {
+            var depends = dat.original[xs[i]].depends;
+            for (var j = 0; j < depends.length; j++) {
+                ds.push(getDirect(depends[j]));
+                is.push(getIndirect(depends[j]));
+            }
+        }
+        ans.push({ name: s, text: res[s].text, back: res[s].back, parents: concatNub(ds), ancestors: concatNub(is) });
+    }
+    return ans;
+}
+function commandFilter(last, dat, query) {
+    queryData = dat;
+    var f = readQuery(query);
+    var res = {};
+    for (queryKey = 0; queryKey < dat.original.length; queryKey++) {
+        queryVal = dat.original[queryKey];
+        if (last && queryVal.built !== 0)
+            continue;
+        var val = recordCopy(queryVal);
+        var ts = queryVal.traces || [];
+        queryVal = val;
+        queryName = queryVal.name;
+        queryBackColor = null;
+        queryTextColor = null;
+        for (var i = 0; i < ts.length; i++) {
+            queryVal.traces = [ts[i]];
+            queryGroup = null;
+            if (f()) {
+                if (queryGroup === null)
+                    queryGroup = ts[i].command;
+                if (!(queryGroup in res))
+                    res[queryGroup] = { items: [ts[i]], text: queryTextColor, back: queryBackColor };
+                else {
+                    var c = res[queryGroup];
+                    c.items.push(ts[i]);
+                    c.text = colorAnd(c.text, queryTextColor);
+                    c.back = colorAnd(c.back, queryBackColor);
+                }
+            }
+        }
+    }
+    return res;
+}
+var CommandTable = (function () {
+    function CommandTable() {
+    }
+    return CommandTable;
+})();
+function commandTable(dat, query) {
+    var res = commandFilter(false, dat, query);
+    var ans = [];
+    for (var s in res) {
+        var xs = res[s].items;
+        var time = 0;
+        for (var _i = 0; _i < xs.length; _i++) {
+            var t_2 = xs[_i];
+            time += t_2.stop - t_2.start;
+        }
+        ans.push({ name: s, count: xs.length, text: res[s].text, back: res[s].back, time: time });
+    }
+    return ans;
+}
+function commandPlot(dat, query, buckets) {
+    var end = dat.summary.maxTraceStopLast;
+    var res = commandFilter(true, dat, query);
+    var ans = {};
+    for (var s in res) {
+        var ts = res[s].items;
+        var xs = [];
+        for (var i = 0; i <= buckets; i++)
+            xs.push(0);
+        for (var _i = 0; _i < ts.length; _i++) {
+            var t_3 = ts[_i];
+            var start = t_3.start * buckets / end;
+            var stop = t_3.stop * buckets / end;
+            if (Math.floor(start) === Math.floor(stop))
+                xs[Math.floor(start)] += stop - start;
+            else {
+                for (var j = Math.ceil(start); j < Math.floor(stop); j++)
+                    xs[j]++;
+                xs[Math.floor(start)] += Math.ceil(start) - start;
+                xs[Math.floor(stop)] += stop - Math.floor(stop);
+            }
+        }
+        ans[s] = { items: xs.slice(0, buckets), back: res[s].back || null };
+    }
+    return ans;
+}
+function readQuery(query) {
+    if (query === "")
+        return function () { return true; };
+    var f;
+    try {
+        f = (new Function("return " + query));
+    }
+    catch (e) {
+        throw { user: true, name: "parse", query: query, message: e.toString() };
+    }
+    return function () {
+        try {
+            return f();
+        }
+        catch (e) {
+            throw { user: true, name: "execution", query: query, message: e.toString() };
+        }
+    };
+}
+var queryData = {};
+var queryKey = 0;
+var queryVal = {};
+var queryName = "";
+var queryGroup = null;
+var queryBackColor = null;
+var queryTextColor = null;
+function childOf(r) { return queryData.dependsOnThis(queryKey, r); }
+function parentOf(r) { return queryData.thisDependsOn(queryKey, r); }
+function ancestorOf(r) { return queryData.dependsOnThisTransitive(queryKey, r); }
+function descendantOf(r) { return queryData.thisDependsOnTransitive(queryKey, r); }
+function descendentOf(r) { return descendantOf(r); }
+function group(x) {
+    if (queryGroup === null)
+        queryGroup = "";
+    queryGroup += (queryGroup === "" ? "" : " ") + x;
+    return true;
+}
+function backColor(c, b) {
+    if (b === void 0) { b = true; }
+    if (b)
+        queryBackColor = c;
+    return true;
+}
+function textColor(c, b) {
+    if (b === void 0) { b = true; }
+    if (b === undefined || b)
+        queryTextColor = c;
+    return true;
+}
+function rename(from, to) {
+    if (to === void 0) { to = ""; }
+    queryName = queryName.replace(from, to);
+    return true;
+}
+function slowestRule() {
+    return queryData.summary.maxExecutionName;
+}
+function leaf() {
+    return queryVal.depends.length === 0;
+}
+function run(i) {
+    if (i === undefined)
+        return queryVal.built;
+    else
+        return queryVal.built === i;
+}
+function unchanged() {
+    return queryVal.changed !== queryVal.built;
+}
+function named(r, groupName) {
+    if (r === undefined)
+        return queryName;
+    var res = execRegExp(r, queryName);
+    if (res === null) {
+        if (groupName === undefined)
+            return false;
+        else {
+            group(groupName);
+            return true;
+        }
+    }
+    if (res.length !== 1) {
+        for (var i = 1; i < res.length; i++)
+            group(res[i]);
+    }
+    return true;
+}
+function command(r, groupName) {
+    var n = (queryVal.traces || []).length;
+    if (r === undefined)
+        return n === 0 ? "" : queryVal.traces[0].command;
+    for (var _i = 0, _a = queryVal.traces; _i < _a.length; _i++) {
+        var t_4 = _a[_i];
+        var res = execRegExp(r, t_4.command);
+        if (res === null)
+            continue;
+        if (res.length !== 1) {
+            for (var j = 1; j < res.length; j++)
+                group(res[j]);
+        }
+        return true;
+    }
+    if (groupName === undefined)
+        return false;
+    else {
+        group(groupName);
+        return true;
+    }
+}
+"use strict";
+function initProgress() {
+    $(function () {
+        $(".version").html("Generated by <a href='http://shakebuild.com'>Shake " + version + "</a>.");
+        $("#output").html("");
+        for (var _i = 0; _i < progress.length; _i++) {
+            var x = progress[_i];
+            var actual = [];
+            var ideal = [];
+            for (var t = 5; t < x.values.length; t++) {
+                var y = x.values[t];
+                actual.push([y.idealSecs, y.actualSecs]);
+                ideal.push([y.idealSecs, y.idealSecs]);
+            }
+            var ys = [{ data: ideal, color: "gray" }, { label: x.name, data: actual, color: "red" }];
+            var div = $("<div class='plot'>");
+            $("#output").append(div);
+            $.plot(div, ys, {
+                xaxis: {
+                    transform: function (v) { return -v; },
+                    inverseTransform: function (v) { return -v; }
+                }
+            });
+        }
+    });
+}
+"use strict";
+function t(a, b, c) { return { start: a, stop: b, command: c }; }
+var dat1 = [{ name: "Functional", built: 0, changed: 3, depends: [], execution: 1, traces: [t(0, 1, "gen")] },
+    { name: "Imperative", built: 0, changed: 0, depends: [], execution: 2, traces: [t(0, 1, "gen"), t(1, 2, "gen")] },
+    { name: "HsSource", built: 3, changed: 3, depends: [], execution: 0 },
+    { name: "Haskell", built: 3, changed: 3, depends: [0, 2], execution: 8, traces: [t(1, 8.9, "ghc")] },
+    { name: "C", built: 0, changed: 0, depends: [1], execution: 15, traces: [t(2, 16.9, "gcc")] },
+    { name: "Cpp", built: 0, changed: 0, depends: [1], execution: 10, traces: [t(2, 10, "gcc")] },
+    { name: "Exe", built: 0, changed: 0, depends: [3, 4, 5], execution: 5, traces: [t(17, 22, "link")] }
+];
+function test() {
+    function assert(b) {
+        if (!b)
+            throw "Assertion failed";
+    }
+    function assertEq(got, want) {
+        if (want != got) {
+            console.log("Wanted: " + want);
+            console.log("Got: " + got);
+            assert(false);
+        }
+    }
+    function assertRegex(want, got) {
+        if (!want.test(got)) {
+            console.log("Wanted: " + want);
+            console.log("Got: " + got);
+            assert(false);
+        }
+    }
+    var tab1 = prepare(dat1);
+    var ssum1 = showSummary(tab1.summary);
+    console.log(ssum1);
+    var want = ["4 runs", "7 rules", "5 rebuilt", "7 traced", "6 in", "build time is 41.00s", "38.80s is traced",
+        "longest rule takes 15.00s", "longest traced command takes 14.90s", "parallelism of 1.40", "22.00s"];
+    assertRegex(new RegExp(want.join(".*")), ssum1.join(" "));
+    var par1 = commandPlot(tab1, "group('x')", 10)['x'];
+    console.log(par1);
+    var pars1 = par1.items.map(function (i) { return Math.round(i * 10) / 10; });
+    assert(listEq(pars1, [1.5, 2, 2, 2, 1.5, 1, 1, 1, 1, 1]));
+    function chk(f, query, n) {
+        var ans = f(tab1, query);
+        console_table(ans);
+        assertEq(ans.length, n);
+    }
+    chk(ruleTable, "", 7);
+    chk(ruleTable, "leaf()", 3);
+    chk(ruleTable, "named(/^(.)/)", 5);
+    chk(commandTable, "", 4);
+    chk(commandTable, "command(/g(.*)/)", 3);
+    chk(ruleTable, "childOf('Imperative')", 2);
+    return "passed";
+}
+function console_table(xs) {
+    if ("table" in console)
+        console["table"](xs);
+    else if (xs.length === 0)
+        console.log("No data");
+    else {
+        var widths = [];
+        var cells = [];
+        for (var i_1 = 0; i_1 <= xs.length; i_1++)
+            cells.push([]);
+        for (var s_1 in xs[0]) {
+            var len = s_1.length;
+            cells[0].push(s_1);
+            for (var i = 0; i < xs.length; i++) {
+                var ss = "" + xs[i][s_1];
+                len = Math.max(len, ss.length);
+                cells[i + 1].push(ss);
+            }
+            widths.push(len);
+        }
+        var s = "";
+        for (var x = 0; x < cells.length; x++) {
+            for (var y = 0; y < widths.length; y++)
+                s += "|" + pad(widths[y], cells[x][y]);
+            s += "|\n";
+        }
+        console.log(s);
+    }
+}
+function pad(n, s) {
+    var res = s;
+    for (var i = s.length; i < n; i++)
+        res += " ";
+    return res;
+}
+"use strict";
+var prepared = prepare(profile);
+var currentTable = null;
+var Report = (function () {
+    function Report(mode_, query_) {
+        if (mode_ === void 0) { mode_ = "summary"; }
+        if (query_ === void 0) { query_ = ""; }
+        this.sort = "time";
+        this.sortRev = false;
+        this.mode = mode_;
+        this.query = query_;
+    }
+    return Report;
+})();
+var report = new Report(null, null);
+function reportEq(r1, r2) {
+    return r1.mode === r2.mode && r1.query === r2.query && r1.sort === r2.sort && r1.sortRev === r2.sortRev;
+}
+function reportToURL(r) {
+    var def = new Report();
+    return "?mode=" + r.mode +
+        (r.query === def.query ? "" : "&query=" + encodeURI(r.query).replace(/\+/g, "%2B")) +
+        ((!r.sortRev && r.sort === def.sort) ? "" :
+            "&sort=" + (r.sortRev ? "!" : "") + r.sort);
+}
+function reportFromURL(s) {
+    if (s === void 0) { s = window.location.search; }
+    var res = new Report();
+    var params = uriQueryParameters(s);
+    if ("mode" in params)
+        res.mode = params["mode"];
+    if ("query" in params)
+        res.query = params["query"];
+    if ("sort" in params) {
+        var sort = params["sort"];
+        res.sortRev = sort.substr(0, 1) == "!";
+        res.sort = sort.substr(res.sortRev ? 1 : 0);
+    }
+    return res;
+}
+function reportFromUser() {
+    return new Report($("#mode").val(), $("#query").val());
+}
+function setReport(set, replace, run) {
+    var report2 = set(recordCopy(report));
+    $("#mode").val(report2.mode);
+    $("#query").val(report2.query);
+    $("#run").enable(false).attr("title", "The current query is displayed");
+    if (reportEq(report, report2))
+        return;
+    report = report2;
+    if (window.history) {
+        var title = report.mode + (report.query === "" ? "" : ": " + report.query);
+        var url = reportToURL(report);
+        try {
+            if (replace)
+                window.history.replaceState(report, title, url);
+            else
+                window.history.pushState(report, title, url);
+        }
+        catch (e) {
+        }
+    }
+    $("#link").attr("href", reportToURL(report));
+    if (run)
+        runReport();
+}
+var rightAlign = { count: null, time: null, cost: null, run: null, leaf: null, unchanged: null };
+var twoColumns = { cost: null, time: null };
+var defaultRevSort = { run: null, name: null };
+function tableSort(x) {
+    if (report.sort === x)
+        setReport(function (r) { r.sortRev = !r.sortRev; return r; }, true, false);
+    else
+        setReport(function (r) { r.sort = x; r.sortRev = x in defaultRevSort; return r; }, true, false);
+    showTable(currentTable);
+}
+function showTable(xs) {
+    currentTable = xs;
+    if (xs.length === 0) {
+        $("#output").html("No data found");
+        return;
+    }
+    if (!(report.sort in xs[0]))
+        setReport(function (r) { return new Report(r.mode, r.query); }, true, false);
+    xs.sort(function (a, b) { return (report.sortRev ? -1 : 1) * (b[report.sort] > a[report.sort] ? 1 : -1); });
+    var res = "<table class='data'><tr class='header'>";
+    for (var s in xs[0]) {
+        if (s === "back" || s === "text")
+            continue;
+        res += s in twoColumns ? "<td colspan='2' style='text-align:center;'" :
+            s in rightAlign ? "<td style='text-align:right;'" :
+                "<td";
+        res += " onclick=\"tableSort('" + s + "')\">" + s;
+        if (s === report.sort)
+            res += " <span class='sort'>" + (report.sortRev ? "&#9650;" : "&#9660;") + "</span>";
+        res += "</td>";
+    }
+    res += "</tr>";
+    for (var _i = 0; _i < xs.length; _i++) {
+        var x = xs[_i];
+        res += "<tr";
+        if (x["back"])
+            res += " style='background-color:" + x["back"] + ";'";
+        if (x["text"])
+            res += " style='color:" + x["text"] + ";'";
+        res += ">";
+        for (var s in xs[0]) {
+            if (s === "text" || s === "back")
+                continue;
+            res += "<td" + (s in rightAlign ? " style='text-align:right;'" : "") + ">";
+            if (s === "count")
+                res += x[s] + " &times;";
+            else if (s === "time" || s === "cost")
+                res += showTime(x[s]) + "</td><td style='text-align:right;'>" + showPerc(x[s] / prepared.summary.sumExecution);
+            else
+                res += x[s];
+            res += "</td>";
+        }
+        res += "</tr>";
+    }
+    res += "</table>";
+    $("#output").html(res);
+}
+var currentPlot = null;
+function showPlot(series, options) {
+    var $output = $("#output");
+    var width = $output.width();
+    var height = $output.height();
+    if (series === null && options === null) {
+        if (width === currentPlot.width && height === currentPlot.height)
+            return;
+        series = currentPlot.series;
+        options = currentPlot.options;
+    }
+    currentPlot = { series: series, options: options, width: width, height: height };
+    var div = $("<div>").width(width - 20).height(height - 10);
+    $("#output").html("").append(div);
+    $.plot(div, series, options);
+}
+window.onresize = function () {
+    if (currentPlot !== null)
+        showPlot(null, null);
+};
+function runReport() {
+    currentTable = null;
+    currentPlot = null;
+    try {
+        switch (report.mode) {
+            case "summary":
+                var res = showSummary(prepared.summary);
+                var s_2 = $("#welcome").html();
+                s_2 += "<ul>";
+                for (var i = 0; i < res.length; i++)
+                    s_2 += "<li>" + res[i] + "</li>";
+                s_2 += "</ul>";
+                s_2 += "<p class='version'>Generated by <a href='http://shakebuild.com'>Shake " + version + "</a>.</p>";
+                $("#output").html(s_2);
+                break;
+            case "cmd-plot":
+                {
+                    var xs = commandPlot(prepared, report.query, 100);
+                    var ys = [];
+                    for (var s_3 in xs) {
+                        var x = xs[s_3].items;
+                        var data = [];
+                        for (var j = 0; j < x.length; j++)
+                            data.push([j, x[j]]);
+                        ys.push({ label: s_3, data: data, color: xs[s_3].back, avg: sum(x) / x.length });
+                    }
+                    if (ys.length === 0) {
+                        $("#output").html("No data found, " +
+                            (prepared.summary.countTraceLast === 0
+                                ? "there were no traced commands in the last run."
+                                : "perhaps your filter is too restrictive?"));
+                    }
+                    else {
+                        ys.sort(function (a, b) { return a.avg - b.avg; });
+                        showPlot(ys, {
+                            legend: { show: true, position: "nw", sorted: "reverse" },
+                            series: { stack: true, lines: { lineWidth: 0, fill: 1 } },
+                            yaxis: { min: 0 },
+                            xaxis: { tickFormatter: function (i) { return showTime(prepared.summary.maxTraceStopLast * i / 100); } }
+                        });
+                    }
+                }
+                break;
+            case "cmd-table":
+                showTable(commandTable(prepared, report.query));
+                break;
+            case "rule-table":
+                showTable(ruleTable(prepared, report.query));
+                break;
+            case "rule-graph":
+                {
+                    var xs = ruleGraph(prepared, report.query);
+                    if (xs.length > 250)
+                        $("#output").html("Viewing a graph with > 250 nodes is not supported, and you have " + xs.length + " nodes. Try grouping more aggressively");
+                    else if (typeof Viz === 'undefined')
+                        $("#output").html("Profile reports do not seem to have been built with GraphViz support, this feature is unavailable.");
+                    else {
+                        var res_1 = "digraph \"\"{";
+                        res_1 += "graph[nodesep=0.15,ranksep=0.3];";
+                        res_1 += "node[fontname=\"sans-serif\",fontsize=9,penwidth=0.5,height=0,width=0];";
+                        res_1 += "edge[penwidth=0.5,arrowsize=0.5];";
+                        for (var i = 0; i < xs.length; i++) {
+                            res_1 += "a" + i + "[label=\"" + xs[i].name.split("\\").join("\\\\").split("\"").join("\\\"") + "\"";
+                            if (xs[i].back)
+                                res_1 += ",style=filled,color=\"" + xs[i].back + "\"";
+                            if (xs[i].text)
+                                res_1 += ",fontcolor=\"" + xs[i].text + "\"";
+                            res_1 += "];";
+                            var parents = xs[i].parents;
+                            for (var j = 0; j < parents.length; j++)
+                                res_1 += "a" + i + "->a" + parents[j] + ";";
+                            var ancestors = xs[i].ancestors;
+                            for (var j = 0; j < ancestors.length; j++)
+                                res_1 += "a" + i + "->a" + ancestors[j] + "[style=dashed];";
+                        }
+                        res_1 += "}";
+                        $("#output").html(Viz(res_1, "svg"));
+                    }
+                }
+                break;
+            case "help":
+                $("#output").html($("#help").html());
+                break;
+            default:
+                throw "Unknown report type: " + report.mode;
+                break;
+        }
+    }
+    catch (e) {
+        if (!(e && e.user))
+            throw e;
+        $("#output").html($("#error").html());
+        for (var s in e)
+            $("#output ." + s).text(e[s]);
+    }
+}
+function example(mode, query) {
+    setReport(function (_) { return new Report(mode, query); }, false, true);
+    return false;
+}
+function initProfile() {
+    $(function () {
+        setReport(function (_) { return reportFromURL(); }, true, true);
+        $("#mode,#query").bind("input change", function () {
+            var mode = $("#mode").val();
+            var query = $("#query").val();
+            var enable = mode !== report.mode || query !== report.query;
+            $("#run").enable(enable).attr("title", enable ? "" : "The current query is displayed");
+            $("#link").attr("href", reportToURL(reportFromUser()));
+        });
+        $("#run").click(function () {
+            setReport(function (_) { return reportFromUser(); }, false, true);
+        });
+        $("#query").keypress(function (e) {
+            if (e.which == 13)
+                $("#run").click();
+        });
+        window.onpopstate = function (e) {
+            setReport(function (_) { return reportFromUser(); }, true, true);
+        };
+        $("a.example").each(function () {
+            var mode = $(this).attr("data-mode");
+            var query = $(this).attr("data-query");
+            if (query === undefined)
+                query = $(this).text();
+            var href = reportToURL(new Report(mode, query));
+            var onclick = "return example(decodeURI('" + encodeURI(mode) + "'),decodeURI('" + encodeURI(query) + "'));";
+            $(this).attr("href", href).attr("target", "_blank")[0].setAttribute("onclick", onclick);
+        });
+        $("a.shake").each(function () {
+            var href = "http://hackage.haskell.org/packages/archive/shake/latest/doc/html/Development-Shake.html#v:" +
+                $(this).text().replace("'", "-39-");
+            $(this).attr("href", href).attr("target", "_blank");
+        });
+    });
+}
+"use strict";
+jQuery.fn.enable = function (x) {
+    return this.each(function () {
+        if (x)
+            $(this).removeAttr('disabled');
+        else
+            $(this).attr('disabled', 'disabled');
+    });
+};
+function uriQueryParameters(s) {
+    var params = {};
+    var a = /\+/g;
+    var r = /([^&=]+)=?([^&]*)/g;
+    var d = function (s) { return decodeURIComponent(s.replace(a, " ")); };
+    var q = s.substring(1);
+    while (true) {
+        var e = r.exec(q);
+        if (!e)
+            break;
+        params[d(e[1])] = d(e[2]);
+    }
+    return params;
+}
+;
+function showTime(x) {
+    function digits(x) { var s = String(x); return s.length === 1 ? "0" + s : s; }
+    if (x >= 3600) {
+        x = Math.round(x / 60);
+        return Math.floor(x / 60) + "h" + digits(x % 60) + "m";
+    }
+    else if (x >= 60) {
+        x = Math.round(x);
+        return Math.floor(x / 60) + "m" + digits(x % 60) + "s";
+    }
+    else
+        return x.toFixed(2) + "s";
+}
+function showPerc(x) {
+    return (x * 100).toFixed(2) + "%";
+}
+function plural(n, not1, is1) {
+    if (not1 === void 0) { not1 = "s"; }
+    if (is1 === void 0) { is1 = ""; }
+    return n === 1 ? is1 : not1;
+}
+function sum(xs) {
+    var res = 0;
+    for (var _i = 0; _i < xs.length; _i++) {
+        var x = xs[_i];
+        res += x;
+    }
+    return res;
+}
+function testRegExp(r, s) {
+    if (typeof r === "string")
+        return s.indexOf(r) !== -1;
+    else
+        return r.test(s);
+}
+function execRegExp(r, s) {
+    if (typeof r === "string")
+        return s.indexOf(r) === -1 ? null : [];
+    else
+        return r.exec(s);
+}
+function listEq(xs, ys) {
+    if (xs.length !== ys.length)
+        return false;
+    for (var i = 0; i < xs.length; i++) {
+        if (xs[i] !== ys[i])
+            return false;
+    }
+    return true;
+}
+function cache(key, op) {
+    var store = {};
+    return function (k) {
+        var s = key(k);
+        if (!(s in store))
+            store[s] = op(k);
+        return store[s];
+    };
+}
+function mapEq(xs, ys) {
+    function f(a, b) {
+        for (var s in a) {
+            if (a[s] !== b[s])
+                return false;
+        }
+        return true;
+    }
+    return f(xs, ys) && f(ys, xs);
+}
+function recordCopy(xs) {
+    return mapCopy(xs);
+}
+function mapCopy(xs) {
+    var res = {};
+    for (var s in xs)
+        res[s] = xs[s];
+    return res;
+}
+function mapUnion(xs, ys) {
+    var res = mapCopy(ys);
+    for (var s in xs)
+        res[s] = xs[s];
+    return res;
+}
+function concatNub(xss) {
+    var res = [];
+    var seen = {};
+    for (var _i = 0; _i < xss.length; _i++) {
+        var xs = xss[_i];
+        for (var _a = 0; _a < xs.length; _a++) {
+            var x = xs[_a];
+            var v = x;
+            if (!(v in seen)) {
+                seen[v] = null;
+                res.push(x);
+            }
+        }
+    }
+    return res;
+}
diff --git a/shake.cabal b/shake.cabal
--- a/shake.cabal
+++ b/shake.cabal
@@ -1,13 +1,13 @@
 cabal-version:      >= 1.10
 build-type:         Simple
 name:               shake
-version:            0.15.5
+version:            0.15.6
 license:            BSD3
 license-file:       LICENSE
 category:           Development, Shake
 author:             Neil Mitchell <ndmitchell@gmail.com>
 maintainer:         Neil Mitchell <ndmitchell@gmail.com>
-copyright:          Neil Mitchell 2011-2015
+copyright:          Neil Mitchell 2011-2016
 synopsis:           Build system library, like Make, but more accurate dependencies.
 description:
     Shake is a Haskell library for writing build systems - designed as a
@@ -31,7 +31,7 @@
     (e.g. compiler version).
 homepage:           http://shakebuild.com
 bug-reports:        https://github.com/ndmitchell/shake/issues
-tested-with:        GHC==7.10.1, GHC==7.8.4, GHC==7.6.3, GHC==7.4.2, GHC==7.2.2
+tested-with:        GHC==8.0.1, GHC==7.10.3, GHC==7.8.4, GHC==7.6.3, GHC==7.4.2
 extra-doc-files:
     CHANGES.txt
     README.md
@@ -63,10 +63,7 @@
     html/viz.js
     html/profile.html
     html/progress.html
-    html/shake-logic.js
-    html/shake-progress.js
-    html/shake-ui.js
-    html/shake-util.js
+    html/shake.js
     docs/manual/build.bat
     docs/manual/Build.hs
     docs/manual/build.sh
@@ -87,7 +84,6 @@
     hs-source-dirs:   src
     build-depends:
         base == 4.*,
-        old-time,
         directory,
         hashable >= 1.1.2.3,
         binary,
@@ -101,14 +97,18 @@
         js-jquery,
         js-flot,
         transformers >= 0.2,
-        extra >= 1.3,
+        extra >= 1.4.3,
         deepseq >= 1.1
 
     if flag(portable)
         cpp-options: -DPORTABLE
+        if impl(ghc < 7.6)
+            build-depends: old-time
     else
         if !os(windows)
             build-depends: unix >= 2.5.1
+    if !os(windows)
+        build-depends: unix
 
     exposed-modules:
         Development.Shake
@@ -116,6 +116,7 @@
         Development.Shake.Command
         Development.Shake.Config
         Development.Shake.FilePath
+        Development.Shake.Forward
         Development.Shake.Rule
         Development.Shake.Util
 
@@ -127,6 +128,7 @@
         Development.Shake.Args
         Development.Shake.ByteString
         Development.Shake.Core
+        Development.Shake.CmdOption
         Development.Shake.Database
         Development.Shake.Demo
         Development.Shake.Derived
@@ -154,6 +156,7 @@
         General.Cleanup
         General.Concurrent
         General.Extra
+        General.FileLock
         General.Intern
         General.Process
         General.String
@@ -167,15 +170,12 @@
     hs-source-dirs:   src
     ghc-options: -main-is Run.main
     main-is: Run.hs
-    ghc-options: -threaded -rtsopts
-    if impl(ghc >= 7.4)
-        ghc-options: "-with-rtsopts=-I0 -qg -qb"
-    else
-        -- -qg/-qb segfaults in GHC 7.2
-        ghc-options: -with-rtsopts=-I0
+    ghc-options: -rtsopts
+    -- GHC bug 7646 means -threaded causes errors
+    if impl(ghc >= 7.8)
+        ghc-options: -threaded "-with-rtsopts=-I0 -qg -qb"
     build-depends:
         base == 4.*,
-        old-time,
         directory,
         hashable >= 1.1.2.3,
         binary,
@@ -189,14 +189,19 @@
         js-jquery,
         js-flot,
         transformers >= 0.2,
-        extra >= 1.3,
-        deepseq >= 1.1
+        extra >= 1.4.3,
+        deepseq >= 1.1,
+        primitive
 
     if flag(portable)
         cpp-options: -DPORTABLE
+        if impl(ghc < 7.6)
+            build-depends: old-time
     else
         if !os(windows)
             build-depends: unix >= 2.5.1
+    if !os(windows)
+        build-depends: unix
 
     other-modules:
         Development.Make.All
@@ -205,19 +210,68 @@
         Development.Make.Rules
         Development.Make.Type
         Development.Ninja.All
+        Development.Ninja.Env
+        Development.Ninja.Lexer
+        Development.Ninja.Parse
+        Development.Ninja.Type
+        Development.Shake
+        Development.Shake.Args
+        Development.Shake.ByteString
+        Development.Shake.Classes
+        Development.Shake.CmdOption
+        Development.Shake.Command
+        Development.Shake.Core
+        Development.Shake.Database
+        Development.Shake.Demo
+        Development.Shake.Derived
+        Development.Shake.Errors
+        Development.Shake.FileInfo
+        Development.Shake.FilePath
+        Development.Shake.FilePattern
+        Development.Shake.Forward
+        Development.Shake.Monad
+        Development.Shake.Pool
+        Development.Shake.Profile
+        Development.Shake.Progress
+        Development.Shake.Resource
+        Development.Shake.Rules.Directory
+        Development.Shake.Rules.File
+        Development.Shake.Rules.Files
+        Development.Shake.Rules.Oracle
+        Development.Shake.Rules.OrderOnly
+        Development.Shake.Rules.Rerun
+        Development.Shake.Shake
+        Development.Shake.Special
+        Development.Shake.Storage
+        Development.Shake.Types
+        Development.Shake.Value
+        General.Bilist
+        General.Binary
+        General.Cleanup
+        General.Concurrent
+        General.Extra
+        General.FileLock
+        General.Intern
+        General.Process
+        General.String
+        General.Template
+        General.Timing
+        Paths_shake
         Run
 
 
 test-suite shake-test
     default-language: Haskell2010
     type: exitcode-stdio-1.0
-    ghc-options: -main-is Test.main
+    ghc-options: -main-is Test.main -with-rtsopts=-K1K
     main-is: Test.hs
     hs-source-dirs: src
-    ghc-options: -threaded -rtsopts
+    ghc-options: -rtsopts
+    -- GHC bug 7646 means -threaded causes errors
+    if impl(ghc >= 7.8)
+        ghc-options: -threaded
     build-depends:
         base == 4.*,
-        old-time,
         directory,
         hashable >= 1.1.2.3,
         binary,
@@ -232,14 +286,18 @@
         js-flot,
         transformers >= 0.2,
         deepseq >= 1.1,
-        extra >= 1.3,
+        extra >= 1.4.3,
         QuickCheck >= 2.0
 
     if flag(portable)
         cpp-options: -DPORTABLE
+        if impl(ghc < 7.6)
+            build-depends: old-time
     else
         if !os(windows)
             build-depends: unix >= 2.5.1
+    if !os(windows)
+        build-depends: unix
 
     other-modules:
         Development.Make.All
@@ -248,10 +306,57 @@
         Development.Make.Rules
         Development.Make.Type
         Development.Ninja.All
+        Development.Ninja.Env
+        Development.Ninja.Lexer
         Development.Ninja.Parse
         Development.Ninja.Type
+        Development.Shake
+        Development.Shake.Args
+        Development.Shake.ByteString
+        Development.Shake.Classes
+        Development.Shake.CmdOption
+        Development.Shake.Command
+        Development.Shake.Config
+        Development.Shake.Core
+        Development.Shake.Database
+        Development.Shake.Demo
+        Development.Shake.Derived
+        Development.Shake.Errors
+        Development.Shake.FileInfo
+        Development.Shake.FilePath
+        Development.Shake.FilePattern
+        Development.Shake.Forward
+        Development.Shake.Monad
+        Development.Shake.Pool
+        Development.Shake.Profile
+        Development.Shake.Progress
+        Development.Shake.Resource
+        Development.Shake.Rule
+        Development.Shake.Rules.Directory
+        Development.Shake.Rules.File
+        Development.Shake.Rules.Files
+        Development.Shake.Rules.Oracle
+        Development.Shake.Rules.OrderOnly
+        Development.Shake.Rules.Rerun
+        Development.Shake.Shake
+        Development.Shake.Special
+        Development.Shake.Storage
+        Development.Shake.Types
+        Development.Shake.Util
+        Development.Shake.Value
+        General.Bilist
+        General.Binary
+        General.Cleanup
+        General.Concurrent
+        General.Extra
+        General.FileLock
+        General.Intern
+        General.Process
+        General.String
+        General.Template
+        General.Timing
+        Paths_shake
         Run
-        Test.Type
         Test.Assume
         Test.Basic
         Test.Benchmark
@@ -263,9 +368,11 @@
         Test.Directory
         Test.Docs
         Test.Errors
+        Test.FileLock
         Test.FilePath
         Test.FilePattern
         Test.Files
+        Test.Forward
         Test.Journal
         Test.Lint
         Test.Live
@@ -275,14 +382,15 @@
         Test.Ninja
         Test.Oracle
         Test.OrderOnly
+        Test.Parallel
         Test.Pool
         Test.Progress
         Test.Random
         Test.Resources
         Test.Self
         Test.Tar
-        Test.Throttle
         Test.Tup
+        Test.Type
         Test.Unicode
         Test.Util
         Test.Verbosity
diff --git a/src/Development/Make/All.hs b/src/Development/Make/All.hs
--- a/src/Development/Make/All.hs
+++ b/src/Development/Make/All.hs
@@ -12,11 +12,14 @@
 import Data.List
 import Data.Maybe
 import Data.Tuple.Extra
-import Control.Monad
+import Control.Applicative
+import Control.Monad.Extra
+import Control.Exception.Extra
 import System.Process
 import System.Exit
 import System.Environment.Extra
 import Control.Monad.Trans.State.Strict
+import Prelude
 
 
 runMakefile :: FilePath -> [String] -> IO (Rules ())
@@ -42,7 +45,7 @@
 
 eval :: Env -> Makefile -> IO [Ruler]
 eval env (Makefile xs) = do
-    (rs, env) <- runStateT (fmap concat $ mapM f xs) env
+    (rs, env) <- runStateT (concatMapM f xs) env
     return [r{cmds=(env,snd $ cmds r)} | r <- rs]
     where
         f :: Stmt -> StateT Env IO [Ruler]
@@ -54,7 +57,7 @@
 
         f Rule{..} = do
             e <- get
-            target <- liftIO $ fmap words $ askEnv e targets
+            target <- liftIO $ words <$> askEnv e targets
             return $ map (\t -> Ruler t (e, prerequisites) (undefined, commands)) target
 
 
@@ -97,7 +100,7 @@
     res <- if "@" `isPrefixOf` x then sys $ drop 1 x
            else putNormal x >> sys x
     when (res /= ExitSuccess) $
-        error $ "System command failed: " ++ x
+        liftIO $ errorIO $ "System command failed: " ++ x
     where sys = quietly . traced (unwords $ take 1 $ words x) . system
 
 
@@ -108,7 +111,7 @@
                       else Nothing
         where rest = length v - (length pre + length post)
               subs = take rest $ drop (length pre) v
-    otherwise -> if pat == v then Just id else Nothing
+    _ -> if pat == v then Just id else Nothing
 
 
 vpath :: [FilePath] -> FilePath -> Action FilePath
diff --git a/src/Development/Make/Parse.hs b/src/Development/Make/Parse.hs
--- a/src/Development/Make/Parse.hs
+++ b/src/Development/Make/Parse.hs
@@ -18,7 +18,7 @@
 
 
 parseMakefile :: String -> Makefile
-parseMakefile xs = Makefile $ rejoin $ concatMap parse $ map comments $ continuations $ lines xs
+parseMakefile xs = Makefile $ rejoin $ concatMap (parse . comments) $ continuations $ lines xs
     where
         continuations (x:y:xs) | "\\" `isSuffixOf` x = continuations $ (init x ++ dropWhile isSpace y):xs
         continuations (x:xs) = x : continuations xs
@@ -65,7 +65,7 @@
 
 
 parseVar :: String -> Expr
-parseVar x = Var x
+parseVar = Var
 
 
 parseCommand :: String -> Command
diff --git a/src/Development/Make/Rules.hs b/src/Development/Make/Rules.hs
--- a/src/Development/Make/Rules.hs
+++ b/src/Development/Make/Rules.hs
@@ -9,13 +9,18 @@
 
 import Control.Monad.IO.Class
 import System.Directory
+import Control.Applicative
+import Prelude
 
-import Development.Shake.Core
-import General.String
+import Development.Shake
+import Development.Shake.Rule
 import Development.Shake.Classes
 import Development.Shake.FilePath
-import Development.Shake.FileInfo
 
+-- Internal imports
+import General.String(BSU, unpackU, packU)
+import Development.Shake.FileInfo(ModTime, getFileInfo)
+
 infix 1 ??>
 
 ---------------------------------------------------------------------
@@ -33,7 +38,7 @@
     deriving (Typeable,Eq,Hashable,Binary,Show,NFData)
 
 instance Rule File_Q File_A where
-    storedValue _ (File_Q x) = fmap (fmap (File_A . Just . fst)) $ getFileInfo x
+    storedValue _ (File_Q x) = fmap (File_A . Just . fst) <$> getFileInfo x
 
 
 defaultRuleFile_ :: Rules ()
diff --git a/src/Development/Ninja/All.hs b/src/Development/Ninja/All.hs
--- a/src/Development/Ninja/All.hs
+++ b/src/Development/Ninja/All.hs
@@ -6,11 +6,6 @@
 import Development.Ninja.Type
 import Development.Ninja.Parse
 import Development.Shake hiding (addEnv)
-import Development.Shake.ByteString
-import Development.Shake.Errors
-import Development.Shake.Rules.File
-import Development.Shake.Rules.OrderOnly
-import General.Timing
 import qualified Data.ByteString as BS8
 import qualified Data.ByteString.Char8 as BS
 
@@ -18,14 +13,23 @@
 import qualified Data.HashMap.Strict as Map
 import qualified Data.HashSet as Set
 import Data.Tuple.Extra
+import Control.Applicative
 import Control.Exception.Extra
 import Control.Monad
 import Data.Maybe
 import Data.Char
 import Data.List.Extra
 import System.Info.Extra
+import Prelude
 
+-- Internal imports
+import General.Timing(addTiming)
+import Development.Shake.ByteString(filepathNormalise, parseMakefile)
+import Development.Shake.Errors(errorStructured)
+import Development.Shake.Rules.File(needBS, neededBS)
+import Development.Shake.Rules.OrderOnly(orderOnlyBS)
 
+
 runNinja :: FilePath -> [String] -> Maybe String -> IO (Maybe (Rules ()))
 runNinja file args (Just "compdb") = do
     dir <- getCurrentDirectory
@@ -47,7 +51,7 @@
     putStr $ printCompDb xs
     return Nothing
 
-runNinja file args (Just x) = error $ "Unknown tool argument, expected 'compdb', got " ++ x
+runNinja file args (Just x) = errorIO $ "Unknown tool argument, expected 'compdb', got " ++ x
 
 runNinja file args tool = do
     addTiming "Ninja parse"
@@ -59,14 +63,14 @@
         multiples <- return $ Map.fromList [(x,(xs,b)) | (xs,b) <- map (first $ map filepathNormalise) multiples, x <- xs]
         rules <- return $ Map.fromList rules
         pools <- fmap Map.fromList $ forM ((BS.pack "console",1):pools) $ \(name,depth) ->
-            fmap ((,) name) $ newResource (BS.unpack name) depth
+            (,) name <$> newResource (BS.unpack name) depth
 
         action $ needBS $ concatMap (resolvePhony phonys) $
             if not $ null args then map BS.pack args
             else if not $ null defaults then defaults
             else Map.keys singles ++ Map.keys multiples
 
-        (\x -> fmap (map BS.unpack . fst) $ Map.lookup (BS.pack x) multiples) &?> \out -> let out2 = map BS.pack out in
+        (\x -> (map BS.unpack . fst) <$> Map.lookup (BS.pack x) multiples) &?> \out -> let out2 = map BS.pack out in
             build needDeps phonys rules pools out2 $ snd $ multiples Map.! head out2
 
         (flip Map.member singles . BS.pack) ?> \out -> let out2 = BS.pack out in
@@ -93,7 +97,7 @@
     needBS $ concatMap (resolvePhony phonys) $ depsNormal ++ depsImplicit
     orderOnlyBS $ concatMap (resolvePhony phonys) depsOrderOnly
     case Map.lookup ruleName rules of
-        Nothing -> error $ "Ninja rule named " ++ BS.unpack ruleName ++ " is missing, required to build " ++ BS.unpack (BS.unwords out)
+        Nothing -> liftIO $ errorIO $ "Ninja rule named " ++ BS.unpack ruleName ++ " is missing, required to build " ++ BS.unpack (BS.unwords out)
         Just Rule{..} -> do
             env <- liftIO $ scopeEnv env
             liftIO $ do
@@ -113,7 +117,7 @@
 
                 let withPool act = case Map.lookup pool pools of
                         _ | BS.null pool -> act
-                        Nothing -> error $ "Ninja pool named " ++ BS.unpack pool ++ " not found, required to build " ++ BS.unpack (BS.unwords out)
+                        Nothing -> liftIO $ errorIO $ "Ninja pool named " ++ BS.unpack pool ++ " not found, required to build " ++ BS.unpack (BS.unwords out)
                         Just r -> withResource r 1 act
 
                 when (description /= "") $ putNormal description
@@ -264,10 +268,10 @@
                     | otherwise -> add (replicate ((a+1) `div` 2) '\\') $ f s ('\"':xs)
             xs -> add (replicate (a+1) '\\') $ f s xs
         f s (x:xs) = add [x] $ f s xs
-        f s [] = [] : []
+        f s [] = [[]]
 
         add a (b:c) = (a++b):c
-        add a [] = a:[]
+        add a [] = [a]
 
 {-
 translate (cmd,args) = unwords $ f cmd : map f args
diff --git a/src/Development/Ninja/Lexer.hs b/src/Development/Ninja/Lexer.hs
--- a/src/Development/Ninja/Lexer.hs
+++ b/src/Development/Ninja/Lexer.hs
@@ -1,10 +1,11 @@
 {-# LANGUAGE PatternGuards #-}
-{-# OPTIONS_GHC -O2 #-}
+-- {-# OPTIONS_GHC -O2 #-} -- fails with GHC 7.10
 -- {-# OPTIONS_GHC -ddump-simpl #-}
 
 -- | Lexing is a slow point, the code below is optimised
 module Development.Ninja.Lexer(Lexeme(..), lexer, lexerFile) where
 
+import Control.Applicative
 import Data.Tuple.Extra
 import qualified Data.ByteString.Char8 as BS
 import qualified Data.ByteString.Unsafe as BS
@@ -15,6 +16,7 @@
 import Foreign.Ptr
 import Foreign.Storable
 import GHC.Exts
+import Prelude
 
 ---------------------------------------------------------------------
 -- LIBRARY BITS
@@ -35,7 +37,7 @@
 
 {-# INLINE span0 #-}
 span0 :: (Char -> Bool) -> Str0 -> (Str, Str0)
-span0 f x = break0 (not . f) x
+span0 f = break0 (not . f)
 
 {-# INLINE break0 #-}
 break0 :: (Char -> Bool) -> Str0 -> (Str, Str0)
@@ -97,17 +99,17 @@
 isVarDot x = x == '.' || isVar x
 
 endsDollar :: Str -> Bool
-endsDollar x = BS.isSuffixOf (BS.singleton '$') x
+endsDollar = BS.isSuffixOf (BS.singleton '$')
 
 dropN :: Str0 -> Str0
 dropN x = if head0 x == '\n' then tail0 x else x
 
 dropSpace :: Str0 -> Str0
-dropSpace x = dropWhile0 (== ' ') x
+dropSpace = dropWhile0 (== ' ')
 
 
 lexerFile :: Maybe FilePath -> IO [Lexeme]
-lexerFile file = fmap lexer $ maybe BS.getContents BS.readFile file
+lexerFile file = lexer <$> maybe BS.getContents BS.readFile file
 
 lexer :: Str -> [Lexeme]
 lexer x = lexerLoop $ Str0 $ x `BS.append` BS.pack "\n\n\0"
@@ -130,6 +132,7 @@
         strip str (Str0 x) = if b `BS.isPrefixOf` x then Just $ Str0 $ BS.drop (BS.length b) x else Nothing
             where b = BS.pack str
 
+lexBind :: Str0 -> [Lexeme]
 lexBind c_x | (c,x) <- list0 c_x = case c of
     '\r' -> lexerLoop x
     '\n' -> lexerLoop x
@@ -137,22 +140,26 @@
     '\0' -> []
     _ -> lexxBind LexBind c_x
 
+lexBuild :: Str0 -> [Lexeme]
 lexBuild x
     | (outputs,x) <- lexxExprs True x
-    , (rule,x) <- span0 isVar $ dropSpace x
+    , (rule,x) <- span0 isVarDot $ dropSpace x
     , (deps,x) <- lexxExprs False $ dropSpace x
     = LexBuild outputs rule deps : lexerLoop x
 
+lexDefault :: Str0 -> [Lexeme]
 lexDefault x
     | (files,x) <- lexxExprs False x
     = LexDefault files : lexerLoop x
 
-lexRule x = lexxName LexRule x
-lexPool x = lexxName LexPool x
-lexInclude x = lexxFile LexInclude x
-lexSubninja x = lexxFile LexSubninja x
-lexDefine x = lexxBind LexDefine x
+lexRule, lexPool, lexInclude, lexSubninja, lexDefine :: Str0 -> [Lexeme]
+lexRule = lexxName LexRule
+lexPool = lexxName LexPool
+lexInclude = lexxFile LexInclude
+lexSubninja = lexxFile LexSubninja
+lexDefine = lexxBind LexDefine
 
+lexxBind :: (Str -> Expr -> Lexeme) -> Str0 -> [Lexeme]
 lexxBind ctor x
     | (var,x) <- span0 isVarDot x
     , ('=',x) <- list0 $ dropSpace x
@@ -160,10 +167,12 @@
     = ctor var exp : lexerLoop x
 lexxBind _ x = error $ show ("parse failed when parsing binding", take0 100 x)
 
+lexxFile :: (Expr -> Lexeme) -> Str0 -> [Lexeme]
 lexxFile ctor x
     | (exp,rest) <- lexxExpr False False $ dropSpace x
     = ctor exp : lexerLoop rest
 
+lexxName :: (Str -> Lexeme) -> Str0 -> [Lexeme]
 lexxName ctor x
     | (name,rest) <- splitLineCont x
     = ctor name : lexerLoop rest
@@ -208,7 +217,7 @@
             '\r' -> f $ dropSpace $ dropN x
             '{' | (name,x) <- span0 isVarDot x, not $ BS.null name, ('}',x) <- list0 x -> Var name $: f x
             _ | (name,x) <- span0 isVar c_x, not $ BS.null name -> Var name $: f x
-            _ -> error $ "Unexpect $ followed by unexpected stuff"
+            _ -> error "Unexpect $ followed by unexpected stuff"
 
 
 splitLineCont :: Str0 -> (Str, Str0)
diff --git a/src/Development/Ninja/Type.hs b/src/Development/Ninja/Type.hs
--- a/src/Development/Ninja/Type.hs
+++ b/src/Development/Ninja/Type.hs
@@ -26,12 +26,12 @@
 
 askExpr :: Env Str Str -> Expr -> IO Str
 askExpr e = f
-    where f (Exprs xs) = fmap BS.concat $ mapM f xs
+    where f (Exprs xs) = BS.concat <$> mapM f xs
           f (Lit x) = return x
           f (Var x) = askVar e x
 
 askVar :: Env Str Str -> Str -> IO Str
-askVar e x = fmap (fromMaybe BS.empty) $ askEnv e x
+askVar e x = fromMaybe BS.empty <$> askEnv e x
 
 addBind :: Env Str Str -> Str -> Expr -> IO ()
 addBind e k v = addEnv e k =<< askExpr e v
diff --git a/src/Development/Shake.hs b/src/Development/Shake.hs
--- a/src/Development/Shake.hs
+++ b/src/Development/Shake.hs
@@ -3,15 +3,15 @@
 --   let us build the file @result.tar@ from the files listed by @result.txt@:
 --
 -- @
---import "Development.Shake"
---import "Development.Shake.FilePath"
+-- import "Development.Shake"
+-- import "Development.Shake.FilePath"
 --
---main = 'shakeArgs' 'shakeOptions' $ do
---    'want' [\"result.tar\"]
---    \"*.tar\" '%>' \\out -> do
---        contents \<- 'readFileLines' $ out 'Development.Shake.FilePath.-<.>' \"txt\"
---        'need' contents
---        'cmd' \"tar -cf\" [out] contents
+-- main = 'shakeArgs' 'shakeOptions' $ do
+--     'want' [\"result.tar\"]
+--     \"*.tar\" '%>' \\out -> do
+--         contents \<- 'readFileLines' $ out 'Development.Shake.FilePath.-<.>' \"txt\"
+--         'need' contents
+--         'cmd' \"tar -cf\" [out] contents
 -- @
 --
 --   We start by importing the modules defining both Shake and routines for manipulating 'FilePath' values.
@@ -35,9 +35,9 @@
 -- * The home page has links to additional information <http://www.shakebuild.com/>, including
 --   a mailing list.
 --
--- * The theory behind Shake is described in an ICFP 2012 paper, /Shake Before Building -- Replacing Make with Haskell/
---   <http://community.haskell.org/~ndm/downloads/paper-shake_before_building-10_sep_2012.pdf>. The associated talk
---   forms a short overview of Shake <http://www.youtube.com/watch?v=xYCPpXVlqFM>.
+-- * The theory behind Shake is described in an ICFP 2012 paper,
+--   <http://community.haskell.org/~ndm/downloads/paper-shake_before_building-10_sep_2012.pdf Shake Before Building -- Replacing Make with Haskell>.
+--   The <http://www.youtube.com/watch?v=xYCPpXVlqFM associated talk> forms a short overview of Shake .
 --
 --   /== WRITING A BUILD SYSTEM ==============================/
 --
@@ -74,11 +74,10 @@
 --
 --   * @-rtsopts@: Allow the setting of further GHC options at runtime.
 --
---   * @-I0@: Disable idle garbage collection. In a build system regularly running many system
---     commands the program appears \"idle\" very often, triggering regular unnecessary garbage collection, stealing
---     resources from the program doing actual work.
+--   * @-I0@: Disable idle garbage collection, to avoid frequent unnecessary garbage collection, see
+--     <http://stackoverflow.com/questions/34588057/why-does-shake-recommend-disabling-idle-garbage-collection/ a full explanation>.
 --
---   * With GHC 7.6 and before, omit @-threaded@: A GHC bug 7646 <http://ghc.haskell.org/trac/ghc/ticket/7646>
+--   * With GHC 7.6 and before, omit @-threaded@: <http://ghc.haskell.org/trac/ghc/ticket/7646 GHC bug 7646>
 --     can cause a race condition in build systems that write files then read them. Omitting @-threaded@ will
 --     still allow your 'cmd' actions to run in parallel, so most build systems will still run in parallel.
 --
@@ -101,7 +100,7 @@
     -- ** Command line
     shakeArgs, shakeArgsWith, shakeOptDescrs,
     -- ** Progress reporting
-    Progress(..), progressSimple, progressDisplay, progressTitlebar, progressProgram,
+    Progress(..), progressSimple, progressDisplay, progressTitlebar, progressProgram, getProgress,
     -- ** Verbosity
     Verbosity(..), getVerbosity, putLoud, putNormal, putQuiet, withVerbosity, quietly,
     -- * Running commands
@@ -109,6 +108,8 @@
     Stdout(..), Stderr(..), Stdouterr(..), Exit(..), CmdTime(..), CmdLine(..),
     CmdResult, CmdString, CmdOption(..),
     addPath, addEnv,
+    -- * Explicit parallelism
+    parallel, forP, par,
     -- * Utility functions
     copyFile', copyFileChanged,
     readFile', readFileLines,
@@ -118,15 +119,16 @@
     -- * File rules
     need, want, (%>), (|%>), (?>), phony, (~>), phonys,
     (&%>), (&?>),
-    orderOnly,
+    orderOnly, orderOnlyAction,
     FilePattern, (?==), (<//>),
     needed, trackRead, trackWrite, trackAllow,
     -- * Directory rules
     doesFileExist, doesDirectoryExist, getDirectoryContents, getDirectoryFiles, getDirectoryDirs,
+    getDirectoryFilesIO,
     -- * Environment rules
     getEnv, getEnvWithDefault,
     -- * Oracle rules
-    addOracle, askOracle, askOracleWith,
+    ShakeValue, addOracle, askOracle, askOracleWith,
     -- * Special rules
     alwaysRerun,
     -- * Resources
diff --git a/src/Development/Shake/Args.hs b/src/Development/Shake/Args.hs
--- a/src/Development/Shake/Args.hs
+++ b/src/Development/Shake/Args.hs
@@ -6,6 +6,7 @@
 import Development.Shake.Types
 import Development.Shake.Core
 import Development.Shake.Demo
+import Development.Shake.FilePath
 import Development.Shake.Rules.File
 import Development.Shake.Progress
 import Development.Shake.Shake
@@ -86,12 +87,12 @@
 --   As an example of a build system that can use either @gcc@ or @distcc@ for compiling:
 --
 -- @
---import System.Console.GetOpt
+-- import System.Console.GetOpt
 --
---data Flags = DistCC deriving Eq
---flags = [Option \"\" [\"distcc\"] (NoArg $ Right DistCC) \"Run distributed.\"]
+-- data Flags = DistCC deriving Eq
+-- flags = [Option \"\" [\"distcc\"] (NoArg $ Right DistCC) \"Run distributed.\"]
 --
---main = 'shakeArgsWith' 'shakeOptions' flags $ \\flags targets -> return $ Just $ do
+-- main = 'shakeArgsWith' 'shakeOptions' flags $ \\flags targets -> return $ Just $ do
 --     if null targets then 'want' [\"result.exe\"] else 'want' targets
 --     let compiler = if DistCC \`elem\` flags then \"distcc\" else \"gcc\"
 --     \"*.o\" '%>' \\out -> do
@@ -115,7 +116,12 @@
         progressRecords = [x | ProgressRecord x <- flagsExtra]
         changeDirectory = listToMaybe [x | ChangeDirectory x <- flagsExtra]
         printDirectory = last $ False : [x | PrintDirectory x <- flagsExtra]
-        shakeOpts = foldl' (flip ($)) baseOpts flagsShake
+        oshakeOpts = foldl' (flip ($)) baseOpts flagsShake
+        shakeOpts = oshakeOpts {shakeLintInside = map (toStandard . normalise . addTrailingPathSeparator) $
+                                                  shakeLintInside oshakeOpts
+                               ,shakeLintIgnore = map toStandard $
+                                                  shakeLintIgnore oshakeOpts
+                               }
 
     -- error if you pass some clean and some dirty with specific flags
     errs <- return $ errs ++ flagsError ++ ["cannot mix " ++ a ++ " and " ++ b | a:b:_ <-
@@ -126,7 +132,7 @@
         showHelp
         exitFailure
 
-    if Help `elem` flagsExtra then do
+    if Help `elem` flagsExtra then
         showHelp
      else if Version `elem` flagsExtra then
         putStrLn $ "Shake build system, version " ++ showVersion version
@@ -152,14 +158,14 @@
                 Just d -> bracket_ (getDataFileName "html" >> setCurrentDirectory d) (setCurrentDirectory curdir)
         shakeOpts <- if null progressRecords then return shakeOpts else do
             t <- offsetTime
-            return shakeOpts{shakeProgress = \p -> do
+            return shakeOpts{shakeProgress = \p ->
                 bracket
                     (forkIO $ shakeProgress shakeOpts p)
                     killThread
                     $ const $ progressDisplay 1 (const $ return ()) $ do
                         p <- p
                         t <- t
-                        forM_ progressRecords $ \file -> do
+                        forM_ progressRecords $ \file ->
                             appendFile file $ show (t,p) ++ "\n"
                         return p
             }
@@ -180,7 +186,7 @@
             in case res of
                 Left err ->
                     if Exception `elem` flagsExtra then
-                        throw err
+                        throwIO err
                     else do
                         putStrLn $ esc "31" $ show err
                         exitFailure
@@ -197,7 +203,7 @@
             putStr $ unlines $ ("Usage: " ++ progName ++ " [options] [target] ...") : "Options:" : showOptDescr opts
 
         wrap :: (a -> b) -> OptDescr (Either String a) -> OptDescr (Either String b)
-        wrap f = fmapOptDescr (either Left (Right . f))
+        wrap = fmapOptDescr . fmap
 
 
 showOptDescr :: [OptDescr a] -> [String]
@@ -227,7 +233,7 @@
 --   in 'ShakeOptions'. The command line flags are @make@ compatible where possbile, but additional
 --   flags have been added for the extra options Shake supports.
 shakeOptDescrs :: [OptDescr (Either String (ShakeOptions -> ShakeOptions))]
-shakeOptDescrs = [fmapOptDescr (either Left (Right . snd)) o | (True, o) <- shakeOptsEx]
+shakeOptDescrs = [fmapOptDescr (fmap snd) o | (True, o) <- shakeOptsEx]
 
 data Extra = ChangeDirectory FilePath
            | Version
@@ -270,6 +276,7 @@
     ,yes $ Option ""  ["digest-and"] (NoArg $ Right ([], \s -> s{shakeChange=ChangeModtimeAndDigest})) "Files change when modtime and digest change."
     ,yes $ Option ""  ["digest-and-input"] (NoArg $ Right ([], \s -> s{shakeChange=ChangeModtimeAndDigestInput})) "Files change on modtime (and digest for inputs)."
     ,yes $ Option ""  ["digest-or"] (NoArg $ Right ([], \s -> s{shakeChange=ChangeModtimeOrDigest})) "Files change when modtime or digest change."
+    ,yes $ Option ""  ["digest-not"] (NoArg $ Right ([], \s -> s{shakeChange=ChangeModtime})) "Files change when modtime changes."
     ,no  $ Option ""  ["exception"] (NoArg $ Right ([Exception], id)) "Throw exceptions directly."
     ,yes $ Option ""  ["flush"] (intArg 1 "flush" "N" (\i s -> s{shakeFlush=Just i})) "Flush metadata every N seconds."
     ,yes $ Option ""  ["never-flush"] (noArg $ \s -> s{shakeFlush=Nothing}) "Never explicitly flush metadata."
@@ -277,7 +284,7 @@
     ,yes $ Option "j" ["jobs"] (optIntArg 0 "jobs" "N" $ \i s -> s{shakeThreads=fromMaybe 0 i}) "Allow N jobs/threads at once [default CPUs]."
     ,yes $ Option "k" ["keep-going"] (noArg $ \s -> s{shakeStaunch=True}) "Keep going when some targets can't be made."
     ,yes $ Option "l" ["lint"] (noArg $ \s -> s{shakeLint=Just LintBasic}) "Perform limited validation after the run."
-    ,yes $ Option ""  ["lint-tracker"] (noArg $ \s -> s{shakeLint=Just LintTracker}) "Use tracker.exe to do validation."
+    ,yes $ Option ""  ["lint-fsatrace"] (noArg $ \s -> s{shakeLint=Just LintFSATrace}) "Use fsatrace to do validation."
     ,yes $ Option ""  ["no-lint"] (noArg $ \s -> s{shakeLint=Nothing}) "Turn off --lint."
     ,yes $ Option ""  ["live"] (OptArg (\x -> Right ([], \s -> s{shakeLiveFiles=shakeLiveFiles s ++ [fromMaybe "live.txt" x]})) "FILE") "List the files that are live [to live.txt]."
     ,yes $ Option "m" ["metadata"] (reqArg "PREFIX" $ \x s -> s{shakeFiles=x}) "Prefix for storing metadata files."
@@ -334,7 +341,7 @@
         outputDebug output Nothing = output
         outputDebug output (Just file) = \v msg -> do
             when (v /= Diagnostic) $ output v msg
-            appendFile file $ unescape msg
+            appendFile file $ unescape msg ++ "\n"
 
         outputColor output v msg = output v $ escape "34" msg
 
diff --git a/src/Development/Shake/ByteString.hs b/src/Development/Shake/ByteString.hs
--- a/src/Development/Shake/ByteString.hs
+++ b/src/Development/Shake/ByteString.hs
@@ -43,13 +43,14 @@
                  | otherwise = Just (BS.last x, BS.init x)
 
 
+-- | Equivalent to @toStandard . normaliseEx@ from "Development.Shake.FilePath".
 filepathNormalise :: BS.ByteString -> BS.ByteString
 filepathNormalise xs
     | isWindows, Just (a,xs) <- BS.uncons xs, sep a, Just (b,_) <- BS.uncons xs, sep b = '/' `BS.cons` f xs
     | otherwise = f xs
     where
         sep = Native.isPathSeparator
-        f o = deslash o $ BS.concat $ (slash:) $ intersperse slash $ reverse $ (BS.empty:) $ g 0 $ reverse $ split $ o
+        f o = deslash o $ BS.concat $ (slash:) $ intersperse slash $ reverse $ (BS.empty:) $ g 0 $ reverse $ split o
 
         deslash o x
             | x == slash = case (pre,pos) of
@@ -68,7 +69,7 @@
         g 0 (x:xs) = x : g 0 xs
         g i (x:xs) = g (i-1) xs
 
-        split xs = BS.splitWith sep xs
+        split = BS.splitWith sep
 
 dotDot = BS.pack ".."
 dot = BS.singleton '.'
diff --git a/src/Development/Shake/Classes.hs b/src/Development/Shake/Classes.hs
--- a/src/Development/Shake/Classes.hs
+++ b/src/Development/Shake/Classes.hs
@@ -5,6 +5,9 @@
     Show(..), Typeable(..), Eq(..), Hashable(..), Binary(..), NFData(..)
     ) where
 
+-- I would probably reexport this module by default in Development.Shake,
+-- but Binary defines 'get', which clashes with the State monad.
+
 import Data.Hashable
 import Data.Typeable
 import Data.Binary
diff --git a/src/Development/Shake/CmdOption.hs b/src/Development/Shake/CmdOption.hs
new file mode 100644
--- /dev/null
+++ b/src/Development/Shake/CmdOption.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+module Development.Shake.CmdOption where
+
+import Data.Data
+import qualified Data.ByteString.Lazy.Char8 as LBS
+
+-- | Options passed to 'command' or 'cmd' to control how processes are executed.
+data CmdOption
+    = Cwd FilePath -- ^ Change the current directory in the spawned process. By default uses this processes current directory.
+    | Env [(String,String)] -- ^ Change the environment variables in the spawned process. By default uses this processes environment.
+    | AddEnv String String -- ^ Add an environment variable in the child process.
+    | RemEnv String -- ^ Remove an environment variable from the child process.
+    | AddPath [String] [String] -- ^ Add some items to the prefix and suffix of the @$PATH@ variable.
+    | Stdin String -- ^ Given as the @stdin@ of the spawned process. By default the @stdin@ is inherited.
+    | StdinBS LBS.ByteString -- ^ Given as the @stdin@ of the spawned process.
+    | FileStdin FilePath -- ^ Take the @stdin@ from a file.
+    | Shell -- ^ Pass the command to the shell without escaping - any arguments will be joined with spaces. By default arguments are escaped properly.
+    | BinaryPipes -- ^ Treat the @stdin@\/@stdout@\/@stderr@ messages as binary. By default 'String' results use text encoding and 'ByteString' results use binary encoding.
+    | Traced String -- ^ Name to use with 'traced', or @\"\"@ for no tracing. By default traces using the name of the executable.
+    | Timeout Double -- ^ Abort the computation after N seconds, will raise a failure exit code. Calls 'interruptProcessGroupOf' and 'terminateProcess', but may sometimes fail to abort the process and not timeout.
+    | WithStdout Bool -- ^ Should I include the @stdout@ in the exception if the command fails? Defaults to 'False'.
+    | WithStderr Bool -- ^ Should I include the @stderr@ in the exception if the command fails? Defaults to 'True'.
+    | EchoStdout Bool -- ^ Should I echo the @stdout@? Defaults to 'True' unless a 'Stdout' result is required or you use 'FileStdout'.
+    | EchoStderr Bool -- ^ Should I echo the @stderr@? Defaults to 'True' unless a 'Stderr' result is required or you use 'FileStderr'.
+    | FileStdout FilePath -- ^ Should I put the @stdout@ to a file.
+    | FileStderr FilePath -- ^ Should I put the @stderr@ to a file.
+    | AutoDeps -- ^ Compute dependencies automatically.
+      deriving (Eq,Ord,Show,Data,Typeable)
diff --git a/src/Development/Shake/Command.hs b/src/Development/Shake/Command.hs
--- a/src/Development/Shake/Command.hs
+++ b/src/Development/Shake/Command.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE FlexibleInstances, TypeSynonymInstances, TypeOperators, ScopedTypeVariables #-}
+{-# LANGUAGE FlexibleInstances, TypeSynonymInstances, TypeOperators, ScopedTypeVariables, NamedFieldPuns #-}
 
 -- | This module provides functions for calling command line programs, primarily
 --   'command' and 'cmd'. As a simple example:
@@ -10,24 +10,24 @@
 --   The functions from this module are now available directly from "Development.Shake".
 --   You should only need to import this module if you are using the 'cmd' function in the 'IO' monad.
 module Development.Shake.Command(
-    command, command_, cmd, unit, CmdArguments,
+    command, command_, cmd, unit, CmdArguments, (:->),
     Stdout(..), Stderr(..), Stdouterr(..), Exit(..), Process(..), CmdTime(..), CmdLine(..),
     CmdResult, CmdString, CmdOption(..),
     addPath, addEnv,
     ) where
 
 import Data.Tuple.Extra
-import Control.Applicative
 import Control.Exception.Extra
 import Control.Monad.Extra
 import Control.Monad.IO.Class
+import Data.Char
 import Data.Either.Extra
 import Data.List.Extra
 import Data.Maybe
 import System.Directory
 import System.Environment.Extra
 import System.Exit
-import System.IO.Extra
+import System.IO.Extra hiding (withTempFile, withTempDir)
 import System.Process
 import System.Info.Extra
 import System.Time.Extra
@@ -35,37 +35,20 @@
 import qualified Data.ByteString as BS
 import qualified Data.ByteString.Lazy.Char8 as LBS
 import General.Process
+import Control.Applicative
 import Prelude
 
+import Development.Shake.CmdOption
 import Development.Shake.Core
 import Development.Shake.FilePath
+import Development.Shake.FilePattern
 import Development.Shake.Types
 import Development.Shake.Rules.File
+import Development.Shake.Derived(writeFile', withTempDir)
 
 ---------------------------------------------------------------------
 -- ACTUAL EXECUTION
 
--- | Options passed to 'command' or 'cmd' to control how processes are executed.
-data CmdOption
-    = Cwd FilePath -- ^ Change the current directory in the spawned process. By default uses this processes current directory.
-    | Env [(String,String)] -- ^ Change the environment variables in the spawned process. By default uses this processes environment.
-    | AddEnv String String -- ^ Add an environment variable in the child process.
-    | AddPath [String] [String] -- ^ Add some items to the prefix and suffix of the @$PATH@ variable.
-    | Stdin String -- ^ Given as the @stdin@ of the spawned process. By default the @stdin@ is inherited.
-    | StdinBS LBS.ByteString -- ^ Given as the @stdin@ of the spawned process.
-    | Shell -- ^ Pass the command to the shell without escaping - any arguments will be joined with spaces. By default arguments are escaped properly.
-    | BinaryPipes -- ^ Treat the @stdin@\/@stdout@\/@stderr@ messages as binary. By default 'String' results use text encoding and 'ByteString' results use binary encoding.
-    | Traced String -- ^ Name to use with 'traced', or @\"\"@ for no tracing. By default traces using the name of the executable.
-    | Timeout Double -- ^ Abort the computation after N seconds, will raise a failure exit code. Calls 'interruptProcessGroupOf' and 'terminateProcess', but may sometimes fail to abort the process and not timeout.
-    | WithStdout Bool -- ^ Should I include the @stdout@ in the exception if the command fails? Defaults to 'False'.
-    | WithStderr Bool -- ^ Should I include the @stderr@ in the exception if the command fails? Defaults to 'True'.
-    | EchoStdout Bool -- ^ Should I echo the @stdout@? Defaults to 'True' unless a 'Stdout' result is required or you use 'FileStdout'.
-    | EchoStderr Bool -- ^ Should I echo the @stderr@? Defaults to 'True' unless a 'Stderr' result is required or you use 'FileStderr'.
-    | FileStdout FilePath -- ^ Should I put the @stdout@ to a file.
-    | FileStderr FilePath -- ^ Should I put the @stderr@ to a file.
-      deriving (Eq,Ord,Show)
-
-
 -- | /Deprecated:/ Use 'AddPath'. This function will be removed in a future version.
 --
 --   Add a prefix and suffix to the @$PATH@ environment variable. For example:
@@ -100,7 +83,7 @@
 addEnv :: MonadIO m => [(String, String)] -> m CmdOption
 addEnv extra = do
     args <- liftIO getEnvironment
-    return $ Env $ extra ++ filter (\(a,b) -> a `notElem` map fst extra) args
+    return $ Env $ extra ++ filter (\(a,_) -> a `notElem` map fst extra) args
 
 
 data Str = Str String | BS BS.ByteString | LBS LBS.ByteString | Unit deriving Eq
@@ -122,121 +105,105 @@
 ---------------------------------------------------------------------
 -- ACTION EXPLICIT OPERATION
 
+-- | Given explicit operations, apply the advance ones, like skip/trace/track/autodep
 commandExplicit :: String -> [CmdOption] -> [Result] -> String -> [String] -> Action [Result]
-commandExplicit funcName copts results exe args = do
-    opts <- getShakeOptions
-    verb <- getVerbosity
+commandExplicit funcName oopts results exe args = do
+    ShakeOptions
+        {shakeCommandOptions,shakeRunCommands
+        ,shakeLint,shakeLintInside,shakeLintIgnore} <- getShakeOptions
+    let fopts = shakeCommandOptions ++ oopts
+    let useShell = Shell `elem` fopts
+    let useLint = shakeLint == Just LintFSATrace
+    let useAutoDeps = AutoDeps `elem` fopts
+    let opts = filter (/= Shell) fopts
 
-    let skipper act = if null results && not (shakeRunCommands opts) then return [] else act
+    let skipper act = if null results && not shakeRunCommands then return [] else act
 
     let verboser act = do
-            let cwd = listToMaybe $ reverse [x | Cwd x <- copts]
-            putLoud $ maybe "" (\x -> "cd " ++ x ++ "; ") cwd ++ saneCommandForUser exe args
+            let cwd = listToMaybe $ reverse [x | Cwd x <- opts]
+            putLoud $ maybe "" (\x -> "cd " ++ x ++ "; ") cwd ++ showCommandForUser2 exe args
+            verb <- getVerbosity
             (if verb >= Loud then quietly else id) act
 
-    let tracer = case reverse [x | Traced x <- copts] of
+    let tracer = case reverse [x | Traced x <- opts] of
             "":_ -> liftIO
             msg:_ -> traced msg
             [] -> traced (takeFileName exe)
 
-    let tracker act = case shakeLint opts of
-            Just LintTracker -> (if isWindows then winTracker else unixTracker) act
-            _ -> act [] exe args
-
-        winTracker act = do
-            (dir, cleanup) <- liftIO newTempDir
-            flip actionFinally cleanup $ do
-                res <- act [] "tracker" $ "/if":dir:"/c":exe:args
-                (rs, ws) <- liftIO $ trackerFiles dir
-                trackRead rs
-                trackWrite ws
-                return res
-
-        unixTracker act = do
-            (file, cleanup) <- liftIO newTempFile
-            flip actionFinally cleanup $ do
-                fsat <- liftIO $ getEnv "FSAT"
-                let vars = [AddEnv "DYLD_INSERT_LIBRARIES" fsat
-                           ,AddEnv "DYLD_FORCE_FLAT_NAMESPACE" "1"
-                           ,AddEnv "FSAT_OUT" file]
-                res <- act vars exe args
-                (rs, ws) <- liftIO $ fsatraceFiles file
-                whitelist <- liftIO unixWhitelist
-                let whitelisted x = any (\w -> (w ++ "/") `isPrefixOf` x) whitelist
-                trackRead $ filter (not . whitelisted) rs
-                trackWrite $ filter (not . whitelisted) ws
-                return res
-
-    skipper $ tracker $ \opts exe args -> verboser $ tracer $ commandExplicitIO funcName (opts++copts) results exe args
+    let tracker act
+            | useLint = fsatrace act
+            | useAutoDeps = autodeps act
+            | useShell = shelled act
+            | otherwise = act exe args
 
+        shelled = runShell (unwords $ exe : args)
+                              
+        ignore = map (?==) shakeLintIgnore
+        ham cwd xs = [makeRelative cwd x | x <- map toStandard xs
+                                         , any (`isPrefixOf` x) shakeLintInside
+                                         , not $ any ($ x) ignore]
 
--- | Given a directory (as passed to tracker /if) report on which files were used for reading/writing
-trackerFiles :: FilePath -> IO ([FilePath], [FilePath])
-trackerFiles dir = do
-    curdir <- getCurrentDirectory
-    let pre = upper curdir ++ "\\"
-    files <- getDirectoryContents dir
-    let f typ = do
-            files <- forM [x | x <- files, takeExtension x == ".tlog", takeExtension (dropExtension $ dropExtension x) == '.':typ] $ \file -> do
-                xs <- readFileEncoding utf16 $ dir </> file
-                return $ filter (not . isPrefixOf "." . takeFileName) . mapMaybe (stripPrefix pre) $ lines xs
-            fmap nubOrd $ mapMaybeM correctCase $ nubOrd $ concat files
-    liftM2 (,) (f "read") (f "write")
+        fsaCmd act opts file
+            | useShell = runShell (unwords $ exe : args) $ \exe args -> act "fsatrace" $ opts : file : "--" : exe : args
+            | otherwise = act "fsatrace" $ opts : file : "--" : exe : args
 
+        fsatrace act = withTempFile $ \file -> do
+            res <- fsaCmd act "rwm" file
+            xs <- liftIO $ parseFSAT <$> readFileUTF8' file
+            cwd <- liftIO getCurrentDirectory
+            let reader (FSATRead x) = Just x; reader _ = Nothing
+                writer (FSATWrite x) = Just x; writer (FSATMove x _) = Just x; writer _ = Nothing
+                existing f = liftIO . filterM doesFileExist . nubOrd . mapMaybe f
+            rs <- existing reader xs
+            ws <- existing writer xs
+            let reads = ham cwd rs
+                writes = ham cwd ws
+            when useAutoDeps $
+                unsafeAllowApply $ needed reads
+            trackRead reads
+            trackWrite writes
+            return res
 
-correctCase :: FilePath -> IO (Maybe FilePath)
-correctCase x = f "" x
-    where
-        f pre "" = return $ Just pre
-        f pre x = do
-            let (a,b) = (takeDirectory1 x, dropDirectory1 x)
-            dir <- getDirectoryContents pre
-            case find ((==) a . upper) dir of
-                Nothing -> return Nothing -- if it can't be found it probably doesn't exist, so assume a file that wasn't really read
-                Just v -> f (pre +/+ v) b
+        autodeps act = withTempFile $ \file -> do
+            res <-  fsaCmd act "r" file
+            pxs <- liftIO $ parseFSAT <$> readFileUTF8' file
+            xs <- liftIO $ filterM doesFileExist [x | FSATRead x <- pxs]
+            cwd <- liftIO getCurrentDirectory
+            unsafeAllowApply $ needNorm $ ham cwd xs
+            return res
 
-        a +/+ b = if null a then b else a ++ "/" ++ b
+    skipper $ tracker $ \exe args -> verboser $ tracer $ commandExplicitIO funcName opts results exe args
 
 
-fsatraceFiles :: FilePath -> IO ([FilePath], [FilePath])
-fsatraceFiles file = do
-    xs <- parseFSAT <$> readFileUTF8 file
-    let reader (FSATRead x) = Just x; reader _ = Nothing
-        writer (FSATWrite x) = Just x; writer (FSATMove x y) = Just x; writer _ = Nothing
-    frs <- liftIO $ filterM doesFileExist $ nubOrd $ map normalise $ mapMaybe reader xs
-    fws <- liftIO $ filterM doesFileExist $ nubOrd $ map normalise $ mapMaybe writer xs
-    return (frs, fws)
+-- | Given a shell command, call the continuation with the sanitised exec-style arguments
+runShell :: String -> (String -> [String] -> Action a) -> Action a
+runShell x act | not isWindows = act "/bin/sh" ["-c",x] -- do exactly what Haskell does
+runShell x act = withTempDir $ \dir -> do
+    let file = dir </> "s.bat"
+    writeFile' file x
+    act "cmd.exe" ["/d/q/c",file]
 
 
-data FSAT = FSATWrite FilePath | FSATRead FilePath | FSATMove FilePath FilePath | FSATDelete FilePath
+-- | Parse the FSATrace structure
+data FSAT
+    = FSATWrite FilePath
+    | FSATRead FilePath
+    | FSATDelete FilePath
+    | FSATMove FilePath FilePath
 
-parseFSAT :: String -> [FSAT] -- any parse errors are skipped
-parseFSAT = mapMaybe (f . wordsBy (== ':')) . lines
-    where f ["w",x] = Just $ FSATWrite x
-          f ["r",x] = Just $ FSATRead x
-          f ["m",x,y] = Just $ FSATMove x y
-          f ["d",x] = Just $ FSATDelete x
+-- | Parse the 'FSAT' entries, ignoring anything you don't understand.
+parseFSAT :: String -> [FSAT]
+parseFSAT = mapMaybe f . lines
+    where f ('w':'|':xs) = Just $ FSATWrite xs
+          f ('r':'|':xs) = Just $ FSATRead xs
+          f ('d':'|':xs) = Just $ FSATDelete xs
+          f ('m':'|':xs) | (xs,'|':ys) <- break (== '|') xs = Just $ FSATMove xs ys
           f _ = Nothing
 
-
-unixWhitelist :: IO [FilePath]
-unixWhitelist = do
-    home <- getEnv "HOME"
-    return [home ++ "/.ghc"
-           ,home ++ "/Library/Haskell"
-           ,home ++ "/Applications"
-           ,home ++ "/.cabal"
-           ,"/Applications"
-           ,"/var/folders"
-           ,"/usr"
-           ,"/Library"
-           ,"/System"
-           ]
-
-
 ---------------------------------------------------------------------
 -- IO EXPLICIT OPERATION
 
+-- | Given a very explicit set of CmdOption, translate them to a General.Process structure
 commandExplicitIO :: String -> [CmdOption] -> [Result] -> String -> [String] -> IO [Result]
 commandExplicitIO funcName opts results exe args = do
     let (grabStdout, grabStderr) = both or $ unzip $ for results $ \r -> case r of
@@ -247,7 +214,11 @@
 
     optEnv <- resolveEnv opts
     let optCwd = let x = last $ "" : [x | Cwd x <- opts] in if x == "" then Nothing else Just x
-    let optStdin = flip mapMaybe opts $ \x -> case x of Stdin x -> Just $ Left x; StdinBS x -> Just $ Right x; _ -> Nothing
+    let optStdin = flip mapMaybe opts $ \x -> case x of
+            Stdin x -> Just $ SrcString x
+            StdinBS x -> Just $ SrcBytes x
+            FileStdin x -> Just $ SrcFile x
+            _ -> Nothing
     let optShell = Shell `elem` opts
     let optBinary = BinaryPipes `elem` opts
     let optAsync = ResultProcess Pid0 `elem` results
@@ -259,7 +230,7 @@
     let optEchoStdout = last $ (not grabStdout && null optFileStdout) : [x | EchoStdout x <- opts]
     let optEchoStderr = last $ (not grabStderr && null optFileStderr) : [x | EchoStderr x <- opts]
 
-    let cmdline = saneCommandForUser exe args
+    let cmdline = showCommandForUser2 exe args
     let bufLBS f = do (a,b) <- buf $ LBS LBS.empty; return (a, (\(LBS x) -> f x) <$> b)
         buf Str{} | optBinary = bufLBS (Str . LBS.unpack)
         buf Str{} = do x <- newBuffer; return ([DestString x | not optAsync], Str . concat <$> readBuffer x)
@@ -268,19 +239,19 @@
         buf Unit  = return ([], return Unit)
     (dStdout, dStderr, resultBuild) :: ([[Destination]], [[Destination]], [Double -> ProcessHandle -> ExitCode -> IO Result]) <-
         fmap unzip3 $ forM results $ \r -> case r of
-            ResultCode _ -> return ([], [], \dur pid ex -> return $ ResultCode ex)
-            ResultTime _ -> return ([], [], \dur pid ex -> return $ ResultTime dur)
-            ResultLine _ -> return ([], [], \dur pid ex -> return $ ResultLine cmdline)
-            ResultProcess _ -> return ([], [], \dur pid ex -> return $ ResultProcess $ Pid pid)
+            ResultCode _ -> return ([], [], \_ _ ex -> return $ ResultCode ex)
+            ResultTime _ -> return ([], [], \dur _ _ -> return $ ResultTime dur)
+            ResultLine _ -> return ([], [], \_ _ _ -> return $ ResultLine cmdline)
+            ResultProcess _ -> return ([], [], \_ pid _ -> return $ ResultProcess $ Pid pid)
             ResultStdout    s -> do (a,b) <- buf s; return (a , [], \_ _ _ -> fmap ResultStdout b)
             ResultStderr    s -> do (a,b) <- buf s; return ([], a , \_ _ _ -> fmap ResultStderr b)
             ResultStdouterr s -> do (a,b) <- buf s; return (a , a , \_ _ _ -> fmap ResultStdouterr b)
 
     exceptionBuffer <- newBuffer
-    po <- resolvePath $ ProcessOpts
+    po <- resolvePath ProcessOpts
         {poCommand = if optShell then ShellCommand $ unwords $ exe:args else RawCommand exe args
         ,poCwd = optCwd, poEnv = optEnv, poTimeout = optTimeout
-        ,poStdin = if optBinary || any isRight optStdin then Right $ LBS.concat $ map (either LBS.pack id) optStdin else Left $ concatMap fromLeft optStdin
+        ,poStdin = [SrcBytes LBS.empty | optBinary && not (null optStdin)] ++ optStdin
         ,poStdout = [DestEcho | optEchoStdout] ++ map DestFile optFileStdout ++ [DestString exceptionBuffer | optWithStdout && not optAsync] ++ concat dStdout
         ,poStderr = [DestEcho | optEchoStderr] ++ map DestFile optFileStderr ++ [DestString exceptionBuffer | optWithStderr && not optAsync] ++ concat dStderr
         ,poAsync = optAsync
@@ -299,7 +270,7 @@
                 cwd ++ extra
     case res of
         Left err -> failure $ show err
-        Right (dur,(pid,ex)) | ex /= ExitSuccess && ResultCode ExitSuccess `notElem` results -> do
+        Right (_,(_,ex)) | ex /= ExitSuccess && ResultCode ExitSuccess `notElem` results -> do
             exceptionBuffer <- readBuffer exceptionBuffer
             let captured = ["Stderr" | optWithStderr] ++ ["Stdout" | optWithStdout]
             failure $
@@ -310,14 +281,16 @@
         Right (dur,(pid,ex)) -> mapM (\f -> f dur pid ex) resultBuild
 
 
+-- | Apply all environment operations, to produce a new environment to use.
 resolveEnv :: [CmdOption] -> IO (Maybe [(String, String)])
 resolveEnv opts
-    | null env, null addEnv, null addPath = return Nothing
-    | otherwise = Just . unique . tweakPath . (++ addEnv) <$>
+    | null env, null addEnv, null addPath, null remEnv = return Nothing
+    | otherwise = Just . unique . tweakPath . (++ addEnv) . filter (flip notElem remEnv . fst) <$>
                   if null env then getEnvironment else return (concat env)
     where
         env = [x | Env x <- opts]
         addEnv = [(x,y) | AddEnv x y <- opts]
+        remEnv = [x | RemEnv x <- opts]
         addPath = [(x,y) | AddPath x y <- opts]
 
         newPath mid = intercalate [searchPathSeparator] $
@@ -339,7 +312,7 @@
     = do
     let progExe = if prog == prog -<.> exe then prog else prog <.> exe
     -- use unsafeInterleaveIO to allow laziness to skip the queries we don't use
-    pathOld <- unsafeInterleaveIO $ fmap (fromMaybe "") $ lookupEnv "PATH"
+    pathOld <- unsafeInterleaveIO $ fromMaybe "" <$> lookupEnv "PATH"
     old <- unsafeInterleaveIO $ findExecutable prog
     new <- unsafeInterleaveIO $ findExecutableWith (splitSearchPath path) progExe
     old2 <- unsafeInterleaveIO $ findExecutableWith (splitSearchPath pathOld) progExe
@@ -357,20 +330,13 @@
 resolvePath po = return po
 
 
+-- | Given a list of directories, and a file name, return the complete path if you can find it.
+--   Like findExecutable, but with a custom PATH.
 findExecutableWith :: [FilePath] -> String -> IO (Maybe FilePath)
 findExecutableWith path x = flip firstJustM (map (</> x) path) $ \s ->
     ifM (doesFileExist s) (return $ Just s) (return Nothing)
 
 
--- Like System.Process, but tweaked to show less escaping,
--- Relies on relatively detailed internals of showCommandForUser.
-saneCommandForUser :: FilePath -> [String] -> String
-saneCommandForUser cmd args = unwords $ map f $ cmd:args
-    where
-        f x = if take (length y - 2) (drop 1 y) == x then x else y
-            where y = showCommandForUser x []
-
-
 ---------------------------------------------------------------------
 -- FIXED ARGUMENT WRAPPER
 
@@ -402,14 +368,14 @@
 --   write helper functions that print out the time of a result.
 --
 -- @
---timer :: ('CmdResult' r, MonadIO m) => (forall r . 'CmdResult' r => m r) -> m r
---timer act = do
---    ('CmdTime' t, 'CmdLine' x, r) <- act
---    liftIO $ putStrLn $ \"Command \" ++ x ++ \" took \" ++ show t ++ \" seconds\"
---    return r
+-- timer :: ('CmdResult' r, MonadIO m) => (forall r . 'CmdResult' r => m r) -> m r
+-- timer act = do
+--     ('CmdTime' t, 'CmdLine' x, r) <- act
+--     liftIO $ putStrLn $ \"Command \" ++ x ++ \" took \" ++ show t ++ \" seconds\"
+--     return r
 --
---run :: IO ()
---run = timer $ 'cmd' \"ghc --version\"
+-- run :: IO ()
+-- run = timer $ 'cmd' \"ghc --version\"
 -- @
 newtype CmdTime = CmdTime {fromCmdTime :: Double}
 
@@ -465,6 +431,7 @@
         where (a1,b1) = cmdResult
               (a2,b2) = cmdResult
 
+cmdResultWith :: forall b c. CmdResult b => (b -> c) -> ([Result], [Result] -> c)
 cmdResultWith f = second (f .) cmdResult
 
 instance (CmdResult x1, CmdResult x2, CmdResult x3) => CmdResult (x1,x2,x3) where
@@ -504,7 +471,7 @@
 --   pass @'WithStderr' 'False'@, which causes no streams to be captured by Shake, and certain programs (e.g. @gcc@)
 --   to detect they are running in a terminal.
 command :: CmdResult r => [CmdOption] -> String -> [String] -> Action r
-command opts x xs = fmap b $ commandExplicit "command" opts a x xs
+command opts x xs = b <$> commandExplicit "command" opts a x xs
     where (a,b) = cmdResult
 
 -- | A version of 'command' where you do not require any results, used to avoid errors about being unable
@@ -516,6 +483,8 @@
 ---------------------------------------------------------------------
 -- VARIABLE ARGUMENT WRAPPER
 
+-- | A type annotation, equivalent to the first argument, but in variable argument contexts,
+--   gives a clue as to what return type is expected (not actually enforced).
 type a :-> t = a
 
 
@@ -569,16 +538,37 @@
     cmdArguments xs x = cmdArguments $ xs ++ arg x
 instance CmdResult r => CmdArguments (Action r) where
     cmdArguments x = case partitionEithers x of
-        (opts, x:xs) -> let (a,b) = cmdResult in fmap b $ commandExplicit "cmd" opts a x xs
+        (opts, x:xs) -> let (a,b) = cmdResult in b <$> commandExplicit "cmd" opts a x xs
         _ -> error "Error, no executable or arguments given to Development.Shake.cmd"
 instance CmdResult r => CmdArguments (IO r) where
     cmdArguments x = case partitionEithers x of
-        (opts, x:xs) -> let (a,b) = cmdResult in fmap b $ commandExplicitIO "cmd" opts a x xs
+        (opts, x:xs) -> let (a,b) = cmdResult in b <$> commandExplicitIO "cmd" opts a x xs
         _ -> error "Error, no executable or arguments given to Development.Shake.cmd"
 
+instance CmdArguments [Either CmdOption String] where
+    cmdArguments = id
+
+
 class Arg a where arg :: a -> [Either CmdOption String]
 instance Arg String where arg = map Right . words
 instance Arg [String] where arg = map Right
 instance Arg CmdOption where arg = return . Left
 instance Arg [CmdOption] where arg = map Left
 instance Arg a => Arg (Maybe a) where arg = maybe [] arg
+
+
+---------------------------------------------------------------------
+-- UTILITIES
+
+-- Copied from Derived. Once Derived no longer exports cmd stuff, import from there.
+withTempFile :: (FilePath -> Action a) -> Action a
+withTempFile act = do
+    (file, del) <- liftIO newTempFile
+    act file `actionFinally` del
+
+-- A better version of showCommandForUser, which doesn't escape so much on Windows
+showCommandForUser2 :: FilePath -> [String] -> String
+showCommandForUser2 cmd args = unwords $ map (\x -> if safe x then x else showCommandForUser x []) $ cmd : args
+    where
+        safe xs = not (null xs) && not (any bad xs)
+        bad x = isSpace x || (x == '\\' && not isWindows) || x `elem` "\"\'"
diff --git a/src/Development/Shake/Config.hs b/src/Development/Shake/Config.hs
--- a/src/Development/Shake/Config.hs
+++ b/src/Development/Shake/Config.hs
@@ -12,7 +12,7 @@
 --   This defines the variable @HEADERS_DIR@ (equal to @\/path\/to\/dir@), and
 --   @CFLAGS@ (equal to @-g -I\/path\/to\/dir -O2@), and also includes the configuration
 --   statements in the file @extra/file.cfg@. The full lexical syntax for configuration
---   files is defined here: <http://martine.github.io/ninja/manual.html#_lexical_syntax>.
+--   files is defined here: <https://ninja-build.org/manual.html#_lexical_syntax>.
 --
 --   To use the configuration file either use 'readConfigFile' to parse the configuration file
 --   and use the values directly, or 'usingConfigFile' and 'getConfig' to track the configuration
@@ -37,14 +37,14 @@
 
 -- | Read a config file, returning a list of the variables and their bindings.
 --   Config files use the Ninja lexical syntax:
---   <http://martine.github.io/ninja/manual.html#_lexical_syntax>
+--   <https://ninja-build.org/manual.html#_lexical_syntax>
 readConfigFile :: FilePath -> IO (Map.HashMap String String)
 readConfigFile = readConfigFileWithEnv []
 
 
 -- | Read a config file with an initial environment, returning a list of the variables and their bindings.
 --   Config files use the Ninja lexical syntax:
---   <http://martine.github.io/ninja/manual.html#_lexical_syntax>
+--   <https://ninja-build.org/manual.html#_lexical_syntax>
 readConfigFileWithEnv :: [(String, String)] -> FilePath -> IO (Map.HashMap String String)
 readConfigFileWithEnv vars file = do
     env <- Ninja.newEnv
diff --git a/src/Development/Shake/Core.hs b/src/Development/Shake/Core.hs
--- a/src/Development/Shake/Core.hs
+++ b/src/Development/Shake/Core.hs
@@ -1,25 +1,20 @@
 {-# LANGUAGE RecordWildCards, GeneralizedNewtypeDeriving, ScopedTypeVariables, PatternGuards #-}
-{-# LANGUAGE ExistentialQuantification, MultiParamTypeClasses #-}
-
-{-# LANGUAGE CPP #-}
-#if __GLASGOW_HASKELL__ >= 704
-{-# LANGUAGE ConstraintKinds #-}
-#endif
+{-# LANGUAGE ExistentialQuantification, MultiParamTypeClasses, ConstraintKinds #-}
 
 module Development.Shake.Core(
     run,
-#if __GLASGOW_HASKELL__ >= 704
     ShakeValue,
-#endif
     Rule(..), Rules, rule, action, withoutActions, alternatives, priority,
-    Action, actionOnException, actionFinally, apply, apply1, traced, getShakeOptions,
+    Action, actionOnException, actionFinally, apply, apply1, traced, getShakeOptions, getProgress,
     trackUse, trackChange, trackAllow,
     getVerbosity, putLoud, putNormal, putQuiet, withVerbosity, quietly,
     Resource, newResource, newResourceIO, withResource, withResources, newThrottle, newThrottleIO,
     newCache, newCacheIO,
-    unsafeExtraThread,
+    unsafeExtraThread, unsafeAllowApply,
+    parallel,
+    orderOnlyAction,
     -- Internal stuff
-    rulesIO, runAfter, unsafeIgnoreDependencies,
+    runAfter
     ) where
 
 import Control.Exception.Extra
@@ -27,13 +22,14 @@
 import Data.Tuple.Extra
 import Control.Concurrent.Extra
 import Control.Monad.Extra
+import Control.Monad.Fix
 import Control.Monad.IO.Class
 import Control.Monad.Trans.Writer.Strict
 import Data.Typeable
 import Data.Function
 import Data.Either.Extra
 import Numeric.Extra
-import Data.List
+import Data.List.Extra
 import qualified Data.HashMap.Strict as Map
 import Data.Maybe
 import Data.IORef
@@ -64,18 +60,6 @@
 ---------------------------------------------------------------------
 -- RULES
 
-#if __GLASGOW_HASKELL__ >= 704
--- | Define an alias for the six type classes required for things involved in Shake 'Development.Shake.Rule's.
---   This alias is only available in GHC 7.4 and above, and requires the @ConstraintKinds@ extension.
---
---   To define your own values meeting the necessary constraints it is convenient to use the extensions
---   @GeneralizedNewtypeDeriving@ and @DeriveDataTypeable@ to write:
---
--- > newtype MyType = MyType (String, Bool) deriving (Show,Typeable,Eq,Hashable,Binary,NFData)
-type ShakeValue a = (Show a, Typeable a, Eq a, Hashable a, Binary a, NFData a)
-#endif
-
-
 -- | Define a pair of types that can be used by Shake rules.
 --   To import all the type classes required see "Development.Shake.Classes".
 --
@@ -102,16 +86,16 @@
 --   provide a similar rule).
 --
 -- @
---newtype File = File FilePath deriving (Show, Typeable, Eq, Hashable, Binary, NFData)
---newtype Modtime = Modtime Double deriving (Show, Typeable, Eq, Hashable, Binary, NFData)
---getFileModtime file = ...
+-- newtype File = File FilePath deriving (Show, Typeable, Eq, Hashable, Binary, NFData)
+-- newtype Modtime = Modtime Double deriving (Show, Typeable, Eq, Hashable, Binary, NFData)
+-- getFileModtime file = ...
 --
---instance Rule File Modtime where
---    storedValue _ (File x) = do
---        exists <- System.Directory.doesFileExist x
---        if exists then Just \<$\> getFileModtime x else return Nothing
---    equalValue _ _ t1 t2 =
---        if t1 == t2 then EqualCheap else NotEqual
+-- instance Rule File Modtime where
+--     storedValue _ (File x) = do
+--         exists <- System.Directory.doesFileExist x
+--         if exists then Just \<$\> getFileModtime x else return Nothing
+--     equalValue _ _ t1 t2 =
+--         if t1 == t2 then EqualCheap else NotEqual
 -- @
 --
 --   This example instance means:
@@ -150,14 +134,7 @@
 --   For rules whose values are not stored externally,
 --   'storedValue' should return 'Just' with a sentinel value
 --   and 'equalValue' should always return 'EqualCheap' for that sentinel.
-class (
-#if __GLASGOW_HASKELL__ >= 704
-    ShakeValue key, ShakeValue value
-#else
-    Show key, Typeable key, Eq key, Hashable key, Binary key, NFData key,
-    Show value, Typeable value, Eq value, Hashable value, Binary value, NFData value
-#endif
-    ) => Rule key value where
+class (ShakeValue key, ShakeValue value) => Rule key value where
 
     -- | /[Required]/ Retrieve the @value@ associated with a @key@, if available.
     --
@@ -179,14 +156,11 @@
 ruleValue = err "ruleValue"
 
 
--- | Define a set of rules. Rules can be created with calls to functions such as 'Development.Shake.%>' or 'action'. Rules are combined
---   with either the 'Monoid' instance, or (more commonly) the 'Monad' instance and @do@ notation. To define your own
---   custom types of rule, see "Development.Shake.Rule".
+-- | Define a set of rules. Rules can be created with calls to functions such as 'Development.Shake.%>' or 'action'.
+--   Rules are combined with either the 'Monoid' instance, or (more commonly) the 'Monad' instance and @do@ notation.
+--   To define your own custom types of rule, see "Development.Shake.Rule".
 newtype Rules a = Rules (WriterT (SRules Action) IO a) -- All IO must be associative/commutative (e.g. creating IORef/MVars)
-    deriving (Monad, Functor, Applicative)
-
-rulesIO :: IO a -> Rules a
-rulesIO = Rules . liftIO
+    deriving (Functor, Applicative, Monad, MonadIO, MonadFix)
 
 newRules :: SRules Action -> Rules ()
 newRules = Rules . tell
@@ -215,8 +189,10 @@
     mappend = liftA2 mappend
 
 
--- | Add a rule to build a key, returning an appropriate 'Action'. All rules at a given priority
---   must be disjoint. Rules have priority 1 by default, but can be modified with 'priority'.
+-- | Add a rule to build a key, returning an appropriate 'Action' if the @key@ matches,
+--   or 'Nothing' otherwise.
+--   All rules at a given priority must be disjoint on all used @key@ values, with at most one match.
+--   Rules have priority 1 by default, which can be modified with 'priority'.
 rule :: Rule key value => (key -> Maybe (Action value)) -> Rules ()
 rule r = newRules mempty{rules = Map.singleton k (k, v, [(1,ARule r)])}
     where k = typeOf $ ruleKey r; v = typeOf $ ruleValue r
@@ -310,10 +286,10 @@
         execute rs = \k -> case filter (not . null) $ map (mapMaybe ($ k)) rs2 of
                [r]:_ -> r
                rs -> liftIO $ errorMultipleRulesMatch (typeKey k) (show k) (length rs)
-            where rs2 = sets [(i, \k -> fmap (fmap newValue) $ r (fromKey k)) | (i,ARule r) <- rs]
+            where rs2 = sets [(i, \k -> fmap newValue <$> r (fromKey k)) | (i,ARule r) <- rs]
 
         sets :: Ord a => [(a, b)] -> [[b]] -- highest to lowest
-        sets = map (map snd) . reverse . groupBy ((==) `on` fst) . sortBy (compare `on` fst)
+        sets = map snd . reverse . groupSort
 
 runStored :: Map.HashMap TypeRep (RuleInfo m) -> Key -> IO (Maybe Value)
 runStored mp k = case Map.lookup (typeKey k) mp of
@@ -347,6 +323,7 @@
     ,globalLint :: String -> IO ()
     ,globalAfter :: IORef [IO ()]
     ,globalTrackAbsent :: IORef [(Key, Key)] -- in rule fst, snd must be absent
+    ,globalProgress :: IO Progress
     }
 
 
@@ -403,6 +380,7 @@
 
     let diagnostic = if shakeVerbosity >= Diagnostic then outputLocked Diagnostic . ("% "++) else const $ return ()
     let output v = outputLocked v . abbreviate shakeAbbreviations
+    diagnostic "Starting run"
 
     except <- newIORef (Nothing :: Maybe (String, ShakeException))
     let raiseError err
@@ -422,6 +400,7 @@
                 ,("Wanted",Just dir)
                 ,("Got",Just now)]
                 ""
+    diagnostic "Starting run 2"
 
     after <- newIORef []
     absent <- newIORef []
@@ -430,29 +409,32 @@
             when shakeTimings printTimings
             resetTimings -- so we don't leak memory
         withNumCapabilities shakeThreads $ do
+            diagnostic "Starting run 3"
             withDatabase opts diagnostic $ \database -> do
                 wait <- newBarrier
-                tid <- forkIO $ flip finally (signalBarrier wait ()) $
-                    shakeProgress $ do
-                        failure <- fmap (fmap fst) $ readIORef except
+                let getProgress = do
+                        failure <- fmap fst <$> readIORef except
                         stats <- progress database
                         return stats{isFailure=failure}
-                addCleanup cleanup $ do
+                tid <- flip forkFinally (const $ signalBarrier wait ()) $
+                    shakeProgress getProgress
+                _ <- addCleanup cleanup $ do
                     killThread tid
                     void $ timeout 1000000 $ waitBarrier wait
 
                 let ruleinfo = createRuleinfo opts rs
                 addTiming "Running rules"
                 runPool (shakeThreads == 1) shakeThreads $ \pool -> do
-                    let s0 = Global database pool cleanup start ruleinfo output opts diagnostic lint after absent
+                    let s0 = Global database pool cleanup start ruleinfo output opts diagnostic lint after absent getProgress
                     let s1 = Local emptyStack shakeVerbosity Nothing [] 0 [] [] []
-                    forM_ (actions rs) $ \act -> do
+                    forM_ (actions rs) $ \act ->
                         addPool pool $ runAction s0 s1 act $ \x -> case x of
                             Left e -> raiseError =<< shakeException s0 (return ["Top-level action/want"]) e
                             Right x -> return x
                 maybe (return ()) (throwIO . snd) =<< readIORef except
+                assertFinishedDatabase database
 
-                when (null $ actions rs) $ do
+                when (null $ actions rs) $
                     when (shakeVerbosity >= Normal) $ output Normal "Warning: No want/action statements, nothing to do"
 
                 when (isJust shakeLint) $ do
@@ -486,8 +468,8 @@
 abbreviate [] = id
 abbreviate abbrev = f
     where
-        -- order so longer appreviations are preferred
-        ordAbbrev = sortBy (flip (compare `on` length . fst)) abbrev
+        -- order so longer abbreviations are preferred
+        ordAbbrev = sortOn (negate . length . fst) abbrev
 
         f [] = []
         f x | (to,rest):_ <- [(to,rest) | (from,to) <- ordAbbrev, Just rest <- [stripPrefix from x]] = to ++ f rest
@@ -495,7 +477,7 @@
 
 
 runAction :: Global -> Local -> Action a -> Capture (Either SomeException a)
-runAction g l (Action x) k = runRAW g l x k
+runAction g l (Action x) = runRAW g l x
 
 
 runAfter :: IO () -> Action ()
@@ -508,22 +490,23 @@
 --   This function requires that appropriate rules have been added with 'rule'.
 --   All @key@ values passed to 'apply' become dependencies of the 'Action'.
 apply :: Rule key value => [key] -> Action [value]
-apply = f -- Don't short-circuit [] as we still want error messages
-    where
-        -- We don't want the forall in the Haddock docs
-        f :: forall key value . Rule key value => [key] -> Action [value]
-        f ks = do
-            let tk = typeOf (err "apply key" :: key)
-                tv = typeOf (err "apply type" :: value)
-            Global{..} <- Action getRO
-            block <- Action $ getsRW localBlockApply
-            whenJust block $ liftIO . errorNoApply tk (fmap show $ listToMaybe ks)
-            case Map.lookup tk globalRules of
-                Nothing -> liftIO $ errorNoRuleToBuildType tk (fmap show $ listToMaybe ks) (Just tv)
-                Just RuleInfo{resultType=tv2} | tv /= tv2 -> liftIO $ errorRuleTypeMismatch tk (fmap show $ listToMaybe ks) tv2 tv
-                _ -> fmap (map fromValue) $ applyKeyValue $ map newKey ks
+apply = applyForall
 
+-- We don't want the forall in the Haddock docs
+-- Don't short-circuit [] as we still want error messages
+applyForall :: forall key value . Rule key value => [key] -> Action [value]
+applyForall ks = do
+    let tk = typeOf (err "apply key" :: key)
+        tv = typeOf (err "apply type" :: value)
+    Global{..} <- Action getRO
+    block <- Action $ getsRW localBlockApply
+    whenJust block $ liftIO . errorNoApply tk (show <$> listToMaybe ks)
+    case Map.lookup tk globalRules of
+        Nothing -> liftIO $ errorNoRuleToBuildType tk (show <$> listToMaybe ks) (Just tv)
+        Just RuleInfo{resultType=tv2} | tv /= tv2 -> liftIO $ errorRuleTypeMismatch tk (show <$> listToMaybe ks) tv2 tv
+        _ -> fmap (map fromValue) $ applyKeyValue $ map newKey ks
 
+
 applyKeyValue :: [Key] -> Action [Value]
 applyKeyValue [] = return []
 applyKeyValue ks = do
@@ -538,8 +521,7 @@
                 liftIO $ globalLint $ "before building " ++ top
                 putWhen Chatty $ "# " ++ show k
                 res <- runExecute globalRules k
-                when (shakeLint globalOptions == Just LintTracker)
-                    trackCheckUsed
+                when (Just LintFSATrace == shakeLint globalOptions) trackCheckUsed
                 Action $ fmap ((,) res) getRW) $ \x -> case x of
                     Left e -> continue . Left . toException =<< shakeException global (showStack globalDatabase stack) e
                     Right (res, Local{..}) -> do
@@ -578,10 +560,24 @@
 getShakeOptions :: Action ShakeOptions
 getShakeOptions = Action $ getsRO globalOptions
 
+-- | Get the current 'Progress' structure, as would be returned by 'shakeProgress'.
+getProgress :: Action Progress
+getProgress = do
+    res <- Action $ getsRO globalProgress
+    liftIO res
 
+
 -- | Write an action to the trace list, along with the start/end time of running the IO action.
 --   The 'Development.Shake.cmd' and 'Development.Shake.command' functions automatically call 'traced'.
 --   The trace list is used for profile reports (see 'shakeReport').
+--
+--   By default 'traced' prints some useful extra context about what
+--   Shake is building, e.g.:
+--
+-- > # traced message (for myobject.o)
+--
+--   To suppress the output of 'traced' (for example you want more control
+--   over the message using 'putNormal'), use the 'quietly' combinator.
 traced :: String -> IO a -> Action a
 traced msg act = do
     Global{..} <- Action getRO
@@ -602,12 +598,19 @@
         liftIO $ globalOutput v msg
 
 
--- | Write a message to the output when the verbosity ('shakeVerbosity') is appropriate.
---   The output will not be interleaved with any other Shake messages
---   (other than those generated by system commands).
-putLoud, putNormal, putQuiet :: String -> Action ()
+-- | Write an unimportant message to the output, only shown when 'shakeVerbosity' is higher than normal ('Loud' or above).
+--   The output will not be interleaved with any other Shake messages (other than those generated by system commands).
+putLoud :: String -> Action ()
 putLoud = putWhen Loud
+
+-- | Write a normal priority message to the output, only supressed when 'shakeVerbosity' is 'Quiet' or 'Silent'.
+--   The output will not be interleaved with any other Shake messages (other than those generated by system commands).
+putNormal :: String -> Action ()
 putNormal = putWhen Normal
+
+-- | Write an important message to the output, only supressed when 'shakeVerbosity' is 'Silent'.
+--   The output will not be interleaved with any other Shake messages (other than those generated by system commands).
+putQuiet :: String -> Action ()
 putQuiet = putWhen Quiet
 
 
@@ -639,13 +642,7 @@
 -- TRACKING
 
 -- | Track that a key has been used by the action preceeding it.
-trackUse ::
-#if __GLASGOW_HASKELL__ >= 704
-    ShakeValue key
-#else
-    (Show key, Typeable key, Eq key, Hashable key, Binary key, NFData key)
-#endif
-    => key -> Action ()
+trackUse :: ShakeValue key => key -> Action ()
 -- One of the following must be true:
 -- 1) you are the one building this key (e.g. key == topStack)
 -- 2) you have already been used by apply, and are on the dependency list
@@ -684,7 +681,7 @@
                 ""
 
         -- check 3b
-        bad <- flip filterM localTrackUsed $ \k -> fmap (not . null) $ lookupDependencies globalDatabase k
+        bad <- flip filterM localTrackUsed $ \k -> (not . null) <$> lookupDependencies globalDatabase k
         unless (null bad) $ do
             let n = length bad
             errorStructured
@@ -694,13 +691,7 @@
 
 
 -- | Track that a key has been changed by the action preceeding it.
-trackChange ::
-#if __GLASGOW_HASKELL__ >= 704
-    ShakeValue key
-#else
-    (Show key, Typeable key, Eq key, Hashable key, Binary key, NFData key)
-#endif
-    => key -> Action ()
+trackChange :: ShakeValue key => key -> Action ()
 -- One of the following must be true:
 -- 1) you are the one building this key (e.g. key == topStack)
 -- 2) someone explicitly gave you permission with trackAllow
@@ -721,21 +712,15 @@
 
 
 -- | Allow any matching key to violate the tracking rules.
-trackAllow ::
-#if __GLASGOW_HASKELL__ >= 704
-    ShakeValue key
-#else
-    (Show key, Typeable key, Eq key, Hashable key, Binary key, NFData key)
-#endif
-    => (key -> Bool) -> Action ()
-trackAllow test = Action $ modifyRW $ \s -> s{localTrackAllows = f : localTrackAllows s}
-    where
-        -- We don't want the forall in the Haddock docs
-        arrow1Type :: forall a b . Typeable a => (a -> b) -> TypeRep
-        arrow1Type _ = typeOf (err "trackAllow" :: a)
+trackAllow :: ShakeValue key => (key -> Bool) -> Action ()
+trackAllow = trackAllowForall
 
-        ty = arrow1Type test
-        f k = typeKey k == ty && test (fromKey k)
+-- We don't want the forall in the Haddock docs
+trackAllowForall :: forall key . ShakeValue key => (key -> Bool) -> Action ()
+trackAllowForall test = Action $ modifyRW $ \s -> s{localTrackAllows = f : localTrackAllows s}
+    where
+        tk = typeOf (err "trackAllow key" :: key)
+        f k = typeKey k == tk && test (fromKey k)
 
 
 ---------------------------------------------------------------------
@@ -771,7 +756,7 @@
 --     'Development.Shake.cmd' \"cl -o\" [out] ...
 -- @
 newResource :: String -> Int -> Rules Resource
-newResource name mx = rulesIO $ newResourceIO name mx
+newResource name mx = liftIO $ newResourceIO name mx
 
 
 -- | Create a throttled resource, given a name (for error messages) and a number of resources (the 'Int') that can be
@@ -804,13 +789,17 @@
 --   we will never exceed an average of 1 request every 5 seconds, we may end up running an unbounded number of
 --   requests simultaneously. If this limitation causes a problem in practice it can be fixed.
 newThrottle :: String -> Int -> Double -> Rules Resource
-newThrottle name count period = rulesIO $ newThrottleIO name count period
+newThrottle name count period = liftIO $ newThrottleIO name count period
 
+unsafeAllowApply :: Action a -> Action a
+unsafeAllowApply  = applyBlockedBy Nothing
 
 blockApply :: String -> Action a -> Action a
-blockApply msg = Action . unmodifyRW f . fromAction
-    where f s0 = (s0{localBlockApply=Just msg}, \s -> s{localBlockApply=localBlockApply s0})
+blockApply = applyBlockedBy . Just
 
+applyBlockedBy :: Maybe String -> Action a -> Action a
+applyBlockedBy reason = Action . unmodifyRW f . fromAction
+    where f s0 = (s0{localBlockApply=reason}, \s -> s{localBlockApply=localBlockApply s0})
 
 -- | Run an action which uses part of a finite resource. For more details see 'Resource'.
 --   You cannot depend on a rule (e.g. 'need') while a resource is held.
@@ -818,7 +807,7 @@
 withResource r i act = do
     Global{..} <- Action getRO
     liftIO $ globalDiagnostic $ show r ++ " waiting to acquire " ++ show i
-    offset <- liftIO $ offsetTime
+    offset <- liftIO offsetTime
     Action $ captureRAW $ \continue -> acquireResource r globalPool i $ continue $ Right ()
     res <- Action $ tryRAW $ fromAction $ blockApply ("Within withResource using " ++ show r) $ do
         offset <- liftIO offset
@@ -847,20 +836,21 @@
 newCacheIO :: (Eq k, Hashable k) => (k -> Action v) -> IO (k -> Action v)
 newCacheIO act = do
     var {- :: Var (Map k (Fence (Either SomeException ([Depends],v)))) -} <- newVar Map.empty
-    return $ \key -> do
+    return $ \key ->
         join $ liftIO $ modifyVar var $ \mp -> case Map.lookup key mp of
             Just bar -> return $ (,) mp $ do
                 res <- liftIO $ testFence bar
-                res <- case res of
-                    Just res -> return res
+                (res,offset) <- case res of
+                    Just res -> return (res, 0)
                     Nothing -> do
                         pool <- Action $ getsRO globalPool
+                        offset <- liftIO offsetTime
                         Action $ captureRAW $ \k -> waitFence bar $ \v ->
-                            addPool pool $ k $ Right v
+                            addPool pool $ do offset <- liftIO offset; k $ Right (v,offset)
                 case res of
                     Left err -> Action $ throwRAW err
                     Right (deps,v) -> do
-                        Action $ modifyRW $ \s -> s{localDepends = deps ++ localDepends s}
+                        Action $ modifyRW $ \s -> s{localDepends = deps ++ localDepends s, localDiscount = localDiscount s + offset}
                         return v
             Nothing -> do
                 bar <- newFence
@@ -898,26 +888,55 @@
 --   To create the result @MyFile.txt.digits@ the file @MyFile.txt@ will be read and counted, but only at most
 --   once per execution.
 newCache :: (Eq k, Hashable k) => (k -> Action v) -> Rules (k -> Action v)
-newCache = rulesIO . newCacheIO
+newCache = liftIO . newCacheIO
 
 
 -- | Run an action without counting to the thread limit, typically used for actions that execute
---   on remote machines using barely any local CPU resources. Unsafe as it allows the 'shakeThreads' limit to be exceeded.
+--   on remote machines using barely any local CPU resources.
+--   Unsafe as it allows the 'shakeThreads' limit to be exceeded.
 --   You cannot depend on a rule (e.g. 'need') while the extra thread is executing.
 --   If the rule blocks (e.g. calls 'withResource') then the extra thread may be used by some other action.
 --   Only really suitable for calling 'cmd'/'command'.
 unsafeExtraThread :: Action a -> Action a
 unsafeExtraThread act = Action $ do
-    global@Global{..} <- getRO
+    Global{..} <- getRO
     stop <- liftIO $ increasePool globalPool
     res <- tryRAW $ fromAction $ blockApply "Within unsafeExtraThread" act
     liftIO stop
     captureRAW $ \continue -> (if isLeft res then addPoolPriority else addPool) globalPool $ continue res
 
 
--- | Ignore any dependencies added by an action.
-unsafeIgnoreDependencies :: Action a -> Action a
-unsafeIgnoreDependencies act = Action $ do
+-- | Execute a list of actions in parallel. In most cases 'need' will be more appropriate to benefit from parallelism.
+parallel :: [Action a] -> Action [a]
+parallel [] = return []
+parallel [x] = fmap return x
+parallel acts = Action $ do
+    global@Global{..} <- getRO
+    local <- getRW
+    -- number of items still to complete, or Nothing for has completed (by either failure or completion)
+    todo :: Var (Maybe Int) <- liftIO $ newVar $ Just $ length acts
+    -- a list of refs where the results go
+    results :: [IORef (Maybe (Either SomeException a))] <- liftIO $ replicateM (length acts) $ newIORef Nothing
+
+    captureRAW $ \continue -> do
+        let resume = do
+                res <- liftIO $ sequence . catMaybes <$> mapM readIORef results
+                continue res
+
+        liftIO $ forM_ (zip acts results) $ \(act, result) -> do
+            let act2 = ifM (liftIO $ isJust <$> readVar todo) act (fail "")
+            addPool globalPool $ runAction global local act2 $ \res -> do
+                writeIORef result $ Just res
+                modifyVar_ todo $ \v -> case v of
+                    Nothing -> return Nothing
+                    Just i | i == 1 || isLeft res -> do resume; return Nothing
+                    Just i -> return $ Just $ i - 1
+
+
+-- | Run an action but do not depend on anything the action uses.
+--   A more general version of 'orderOnly'.
+orderOnlyAction :: Action a -> Action a
+orderOnlyAction act = Action $ do
     pre <- getsRW localDepends
     res <- fromAction act
     modifyRW $ \s -> s{localDepends=pre}
diff --git a/src/Development/Shake/Database.hs b/src/Development/Shake/Database.hs
--- a/src/Development/Shake/Database.hs
+++ b/src/Development/Shake/Database.hs
@@ -4,7 +4,7 @@
 
 module Development.Shake.Database(
     Trace(..),
-    Database, withDatabase,
+    Database, withDatabase, assertFinishedDatabase,
     listDepends, lookupDependencies,
     Ops(..), build, Depends,
     progress,
@@ -110,7 +110,7 @@
     {result :: Value -- the result associated with the Key
     ,built :: {-# UNPACK #-} !Step -- when it was actually run
     ,changed :: {-# UNPACK #-} !Step -- the step for deciding if it's valid
-    ,depends :: [[Id]] -- dependencies
+    ,depends :: [[Id]] -- dependencies (outer list is a strict ordered series, inner list is parallel)
     ,execution :: {-# UNPACK #-} !Float -- how long it took when it was last run (seconds)
     ,traces :: [Trace] -- a trace of the expensive operations (start/end in seconds since beginning of run)
     } deriving Show
@@ -185,7 +185,7 @@
 
 -- | Return either an exception (crash), or (how much time you spent waiting, the value)
 build :: Pool -> Database -> Ops -> Stack -> [Key] -> Capture (Either SomeException (Seconds,Depends,[Value]))
-build pool Database{..} Ops{..} stack ks continue = do
+build pool database@Database{..} Ops{..} stack ks continue =
     join $ withLock lock $ do
         is <- forM ks $ \k -> do
             is <- readIORef intern
@@ -198,10 +198,15 @@
                     return i
 
         whenJust (checkStack is stack) $ \bad -> do
+            -- everything else gets thrown via Left and can be Staunch'd
+            -- recursion in the rules is considered a worse error, so fails immediately
             status <- readIORef status
-            uncurry errorRuleRecursion $ case Map.lookup bad status of
+            let Stack _ xs _ = stack
+            stack <- return $ reverse $ map (maybe "<unknown>" (show . fst) . flip Map.lookup status) $ bad:xs
+            (tk, tname) <- return $ case Map.lookup bad status of
                 Nothing -> (Nothing, Nothing)
                 Just (k,_) -> (Just $ typeKey k, Just $ show k)
+            errorRuleRecursion stack tk tname
 
         vs <- mapM (reduce stack) is
         let errs = [e | Error e <- vs]
@@ -270,7 +275,7 @@
         run stack i k r = do
             w <- newWaiting r
             addPool pool $ do
-                let reply res =  do
+                let reply res = do
                         ans <- withLock lock $ do
                             ans <- i #= (k, res)
                             runWaiting w
@@ -279,7 +284,7 @@
                             Ready r -> do
                                 diagnostic $ "result " ++ atom k ++ " = "++ atom (result r) ++
                                              " " ++ (if built r == changed r then "(changed)" else "(unchanged)")
-                                journal i (k, Loaded r) -- leave the DB lock before appending
+                                journal i (k, Loaded r) -- we leave the DB lock before appending
                             Error _ -> do
                                 diagnostic $ "result " ++ atom k ++ " = error"
                                 journal i (k, Missing)
@@ -358,6 +363,15 @@
 ---------------------------------------------------------------------
 -- QUERY DATABASE
 
+assertFinishedDatabase :: Database -> IO ()
+assertFinishedDatabase Database{..} = do
+    -- if you have anyone Waiting, and are not exiting with an error, then must have a complex recursion (see #400)
+    status <- readIORef status
+    let bad = [key | (_, (key, Waiting{})) <- Map.toList status]
+    when (bad /= []) $
+        errorComplexRecursion (map show bad)
+
+
 -- | Given a map of representing a dependency order (with a show for error messages), find an ordering for the items such
 --   that no item points to an item before itself.
 --   Raise an error if you end up with a cycle.
@@ -378,7 +392,7 @@
                     "Internal invariant broken, database seems to be cyclic" :
                     map ("    " ++) bad ++
                     ["... plus " ++ show (length badOverflow) ++ " more ..." | not $ null badOverflow]
-            where (bad,badOverflow) = splitAt 10 $ [shw i | (i, Just _) <- Map.toList mp]
+            where (bad,badOverflow) = splitAt 10 [shw i | (i, Just _) <- Map.toList mp]
 
         f (x:xs) mp = x : f (now++xs) later
             where Just free = Map.lookupDefault (Just []) x mp
@@ -400,7 +414,7 @@
 
 toReport :: Database -> IO [ProfileEntry]
 toReport Database{..} = do
-    status <- fmap (removeStep . resultsOnly) $ readIORef status
+    status <- (removeStep . resultsOnly) <$> readIORef status
     let order = let shw i = maybe "<unknown>" (show . fst) $ Map.lookup i status
                 in dependencyOrder shw $ Map.map (concat . depends . snd) status
         ids = Map.fromList $ zip order [0..]
@@ -468,7 +482,7 @@
         return $ map (fst . fromJust . flip Map.lookup status) xs
 
 lookupDependencies :: Database -> Key -> IO [Key]
-lookupDependencies Database{..} k = do
+lookupDependencies Database{..} k =
     withLock lock $ do
         intern <- readIORef intern
         status <- readIORef status
@@ -531,4 +545,4 @@
     putWith ctx Missing = putWord8 0
     putWith ctx (Loaded x) = putWord8 1 >> putWith ctx x
     putWith ctx x = err $ "putWith, Cannot write Status with constructor " ++ statusType x
-    getWith ctx = do i <- getWord8; if i == 0 then return Missing else fmap Loaded $ getWith ctx
+    getWith ctx = do i <- getWord8; if i == 0 then return Missing else Loaded <$> getWith ctx
diff --git a/src/Development/Shake/Demo.hs b/src/Development/Shake/Demo.hs
--- a/src/Development/Shake/Demo.hs
+++ b/src/Development/Shake/Demo.hs
@@ -5,6 +5,7 @@
 import Paths_shake
 import Development.Shake.Command
 
+import Control.Applicative
 import Control.Exception.Extra
 import Control.Monad
 import Data.Char
@@ -17,13 +18,14 @@
 import Development.Shake.FilePath(exe)
 import System.IO
 import System.Info.Extra
+import Prelude
 
 
 demo :: Bool -> IO ()
 demo auto = do
     hSetBuffering stdout NoBuffering
     putStrLn $ "% Welcome to the Shake v" ++ showVersion version ++ " demo mode!"
-    putStr $ "% Detecting machine configuration... "
+    putStr "% Detecting machine configuration... "
 
     -- CONFIGURE
 
@@ -48,13 +50,13 @@
     require shakeLib "% You don't have the 'shake' library installed with GHC, which is required to run the demo."
     require hasManual "% You don't have the Shake data files installed, which are required to run the demo."
 
-    empty <- fmap (null . filter (not . all (== '.'))) $ getDirectoryContents "."
+    empty <- (not . any (not . all (== '.'))) <$> getDirectoryContents "."
     dir <- if empty then getCurrentDirectory else do
         home <- getHomeDirectory
         dir <- getDirectoryContents home
         return $ home </> head (map ("shake-demo" ++) ("":map show [2..]) \\ dir)
 
-    putStrLn $ "% The Shake demo uses an empty directory, OK to use:"
+    putStrLn "% The Shake demo uses an empty directory, OK to use:"
     putStrLn $ "%     " ++ dir
     b <- yesNo auto
     require b "% Please create an empty directory to run the demo from, then run 'shake --demo' again."
@@ -63,7 +65,7 @@
     createDirectoryIfMissing True dir
     forM_ ["Build.hs","main.c","constants.c","constants.h","build" <.> if isWindows then "bat" else "sh"] $ \file ->
         copyFile (manual </> file) (dir </> file)
-    when (not isWindows) $ do
+    unless isWindows $ do
          p <- getPermissions $ dir </> "build.sh"
          setPermissions (dir </> "build.sh") p{executable=True}
     putStrLn "done"
@@ -111,7 +113,7 @@
 -- | Require the user to press @y@ before continuing.
 yesNo :: Bool -> IO Bool
 yesNo auto = do
-    putStr $ "% [Y/N] (then ENTER): "
+    putStr "% [Y/N] (then ENTER): "
     x <- if auto then putLine "y" else fmap (map toLower) getLine
     if "y" `isPrefixOf` x then
         return True
diff --git a/src/Development/Shake/Derived.hs b/src/Development/Shake/Derived.hs
--- a/src/Development/Shake/Derived.hs
+++ b/src/Development/Shake/Derived.hs
@@ -5,9 +5,12 @@
     readFile', readFileLines,
     writeFile', writeFileLines, writeFileChanged,
     withTempFile, withTempDir,
-    getHashedShakeVersion
+    getHashedShakeVersion,
+    par, forP
     ) where
 
+import Control.Applicative
+import Control.Exception.Extra
 import Control.Monad.Extra
 import Control.Monad.IO.Class
 import System.Process
@@ -21,6 +24,7 @@
 import Development.Shake.Types
 import qualified Data.ByteString as BS
 import Data.Hashable
+import Prelude
 
 
 -- | Get a checksum of a list of files, suitable for using as `shakeVersion`.
@@ -45,8 +49,8 @@
 
 
 checkExitCode :: String -> ExitCode -> Action ()
-checkExitCode cmd ExitSuccess = return ()
-checkExitCode cmd (ExitFailure i) = error $ "System command failed (code " ++ show i ++ "):\n" ++ cmd
+checkExitCode _ ExitSuccess = return ()
+checkExitCode cmd (ExitFailure i) = liftIO $ errorIO $ "System command failed (code " ++ show i ++ "):\n" ++ cmd
 
 {-# DEPRECATED system' "Use 'command' or 'cmd'" #-}
 {-# DEPRECATED systemCwd "Use 'command' or 'cmd' with 'Cwd'" #-}
@@ -115,7 +119,7 @@
     liftIO $ copyFile old new
 
 
--- | @copyFile' old new@ copies the existing file from @old@ to @new@, if the contents have changed.
+-- | @copyFileChanged old new@ copies the existing file from @old@ to @new@, if the contents have changed.
 --   The @old@ file will be tracked as a dependency.
 copyFileChanged :: FilePath -> FilePath -> Action ()
 copyFileChanged old new = do
@@ -124,7 +128,7 @@
         withBinaryFile old ReadMode $ \h1 -> withBinaryFile new ReadMode $ \h2 ->
             liftM2 (==) (hFileSize h1) (hFileSize h2) &&^
                 liftM2 (==) (BS.hGetContents h1) (BS.hGetContents h2)
-    when (not eq) $ do
+    unless eq $ do
         putLoud $ "Copying from " ++ old ++ " to " ++ new
         -- copyFile does a lot of clever stuff with permissions etc, so make sure we just reuse it
         liftIO $ copyFile old new
@@ -178,3 +182,12 @@
 withTempDir act = do
     (dir,del) <- liftIO newTempDir
     act dir `actionFinally` del
+
+
+-- | A 'parallel' version of 'forM'.
+forP :: [a] -> (a -> Action b) -> Action [b]
+forP xs f = parallel $ map f xs
+
+-- | Execute two operations in parallel, based on 'parallel'.
+par :: Action a -> Action b -> Action (a,b)
+par a b = do [Left a, Right b] <- parallel [Left <$> a, Right <$> b]; return (a,b)
diff --git a/src/Development/Shake/Errors.hs b/src/Development/Shake/Errors.hs
--- a/src/Development/Shake/Errors.hs
+++ b/src/Development/Shake/Errors.hs
@@ -5,7 +5,7 @@
     ShakeException(..),
     errorStructured, err,
     errorNoRuleToBuildType, errorRuleTypeMismatch, errorIncompatibleRules,
-    errorMultipleRulesMatch, errorRuleRecursion, errorNoApply,
+    errorMultipleRulesMatch, errorRuleRecursion, errorComplexRecursion, errorNoApply,
     ) where
 
 import Data.Tuple.Extra
@@ -29,7 +29,10 @@
 
 
 errorStructured :: String -> [(String, Maybe String)] -> String -> IO a
-errorStructured msg args hint = errorIO $ unlines $
+errorStructured msg args hint = errorIO $ errorStructuredContents msg args hint
+
+errorStructuredContents :: String -> [(String, Maybe String)] -> String -> String
+errorStructuredContents msg args hint = unlines $
         [msg ++ ":"] ++
         ["  " ++ a ++ [':' | a /= ""] ++ replicate (as - length a + 2) ' ' ++ b | (a,b) <- args2] ++
         [hint | hint /= ""]
@@ -84,13 +87,22 @@
     (if count == 0 then "Either add a rule that produces the above key, or stop requiring the above key"
      else "Modify your rules/defaultRules so only one can produce the above key")
 
-errorRuleRecursion :: Maybe TypeRep -> Maybe String -> IO a
-errorRuleRecursion tk k = errorStructured -- may involve both rules and oracle, so report as a rule
+errorRuleRecursion :: [String] -> Maybe TypeRep -> Maybe String -> IO a
+-- may involve both rules and oracle, so report as only rules
+errorRuleRecursion stack tk k = throwIO $ wrap $ toException $ ErrorCall $ errorStructuredContents
     "Build system error - recursion detected"
     [("Key type",fmap show tk)
     ,("Key value",k)]
     "Rules may not be recursive"
+    where
+        wrap = if null stack then id else toException . ShakeException (last stack) stack
 
+errorComplexRecursion :: [String] -> IO a
+errorComplexRecursion ks = errorStructured
+    "Build system error - indirect recursion detected"
+    [("Key value " ++ show i, Just k) | (i, k) <- zip [1..] ks]
+    "Rules may not be recursive"
+
 errorDuplicateOracle :: TypeRep -> Maybe String -> [TypeRep] -> IO a
 errorDuplicateOracle tk k tvs = errorStructured
     "Build system error - duplicate oracles for the same question type"
@@ -117,10 +129,10 @@
 -- | Error representing all expected exceptions thrown by Shake.
 --   Problems when executing rules will be raising using this exception type.
 data ShakeException = ShakeException
-        {shakeExceptionTarget :: String -- ^ The target that was being built when the exception occured.
-        ,shakeExceptionStack :: [String]  -- ^ The stack of targets, where the 'shakeExceptionTarget' is last.
-        ,shakeExceptionInner :: SomeException -- ^ The underlying exception that was raised.
-        }
+    {shakeExceptionTarget :: String -- ^ The target that was being built when the exception occured.
+    ,shakeExceptionStack :: [String]  -- ^ The stack of targets, where the 'shakeExceptionTarget' is last.
+    ,shakeExceptionInner :: SomeException -- ^ The underlying exception that was raised.
+    }
     deriving Typeable
 
 instance Exception ShakeException
diff --git a/src/Development/Shake/FileInfo.hs b/src/Development/Shake/FileInfo.hs
--- a/src/Development/Shake/FileInfo.hs
+++ b/src/Development/Shake/FileInfo.hs
@@ -6,7 +6,7 @@
     getFileHash, getFileInfo
     ) where
 
-import Control.Exception
+import Control.Exception.Extra
 import Development.Shake.Classes
 import General.String
 import qualified Data.ByteString.Lazy as LBS
@@ -15,17 +15,16 @@
 import Numeric
 import System.IO
 
-#if __GLASGOW_HASKELL__ < 704
-#define PORTABLE 1
-#endif
-
 #if defined(PORTABLE)
 import System.IO.Error
 import System.Directory
 import Data.Time
+#if __GLASGOW_HASKELL__ < 706
 import System.Time
+#endif
 
 #elif defined(mingw32_HOST_OS)
+import Control.Monad
 import qualified Data.ByteString.Char8 as BS
 import Foreign
 import Foreign.C.Types
@@ -71,20 +70,32 @@
     evaluate res
     return res
 
+-- If the result isn't strict then we are referencing a much bigger structure,
+-- and it causes a space leak I don't really understand on Linux when running
+-- the 'tar' test, followed by the 'benchmark' test.
+-- See this blog post: http://neilmitchell.blogspot.co.uk/2015/09/three-space-leaks.html
+result :: Word32 -> Word32 -> IO (Maybe (ModTime, FileSize))
+result x y = do
+    x <- evaluate $ fileInfo x
+    y <- evaluate $ fileInfo y
+    return $! Just (x, y)
 
+
 getFileInfo :: BSU -> IO (Maybe (ModTime, FileSize))
 
 #if defined(PORTABLE)
 -- Portable fallback
-getFileInfo x = handleJust (\e -> if isDoesNotExistError e then Just () else Nothing) (const $ return Nothing) $ do
+getFileInfo x = handleBool isDoesNotExistError (const $ return Nothing) $ do
     let file = unpackU x
     time <- getModificationTime file
     size <- withFile file ReadMode hFileSize
-    return $ Just (fileInfo $ extractFileTime time, fileInfo $ fromIntegral size)
+    result (extractFileTime time) (fromIntegral size)
 
 -- deal with difference in return type of getModificationTime between directory versions
 class ExtractFileTime a where extractFileTime :: a -> Word32
+#if __GLASGOW_HASKELL__ < 706
 instance ExtractFileTime ClockTime where extractFileTime (TOD t _) = fromIntegral t
+#endif
 instance ExtractFileTime UTCTime where extractFileTime = floor . fromRational . toRational . utctDayTime
 
 
@@ -92,25 +103,41 @@
 -- Directly against the Win32 API, twice as fast as the portable version
 getFileInfo x = BS.useAsCString (unpackU_ x) $ \file ->
     alloca_WIN32_FILE_ATTRIBUTE_DATA $ \fad -> do
-        res <- c_getFileAttributesExA file 0 fad
-        let peek = do mt <- peekLastWriteTimeLow fad; sz <- peekFileSizeLow fad; return $ Just (fileInfo mt, fileInfo sz)
+        res <- c_GetFileAttributesExA file 0 fad
+        code <- peekFileAttributes fad
+        let peek = do
+                code <- peekFileAttributes fad
+                if testBit code 4 then
+                    errorIO $ "getFileInfo, expected a file, got a directory: " ++ unpackU x
+                 else
+                    join $ liftM2 result (peekLastWriteTimeLow fad) (peekFileSizeLow fad)
         if res then
             peek
          else if requireU x then withCWString (unpackU x) $ \file -> do
-            res <- c_getFileAttributesExW file 0 fad
+            res <- c_GetFileAttributesExW file 0 fad
             if res then peek else return Nothing
          else
             return Nothing
 
-foreign import stdcall unsafe "Windows.h GetFileAttributesExA" c_getFileAttributesExA :: Ptr CChar  -> Int32 -> Ptr WIN32_FILE_ATTRIBUTE_DATA -> IO Bool
-foreign import stdcall unsafe "Windows.h GetFileAttributesExW" c_getFileAttributesExW :: Ptr CWchar -> Int32 -> Ptr WIN32_FILE_ATTRIBUTE_DATA -> IO Bool
+#ifdef x86_64_HOST_ARCH
+#define CALLCONV ccall
+#else
+#define CALLCONV stdcall
+#endif
 
+foreign import CALLCONV unsafe "Windows.h GetFileAttributesExA" c_GetFileAttributesExA :: Ptr CChar  -> Int32 -> Ptr WIN32_FILE_ATTRIBUTE_DATA -> IO Bool
+foreign import CALLCONV unsafe "Windows.h GetFileAttributesExW" c_GetFileAttributesExW :: Ptr CWchar -> Int32 -> Ptr WIN32_FILE_ATTRIBUTE_DATA -> IO Bool
+
 data WIN32_FILE_ATTRIBUTE_DATA
 
 alloca_WIN32_FILE_ATTRIBUTE_DATA :: (Ptr WIN32_FILE_ATTRIBUTE_DATA -> IO a) -> IO a
 alloca_WIN32_FILE_ATTRIBUTE_DATA act = allocaBytes size_WIN32_FILE_ATTRIBUTE_DATA act
     where size_WIN32_FILE_ATTRIBUTE_DATA = 36
 
+peekFileAttributes :: Ptr WIN32_FILE_ATTRIBUTE_DATA -> IO Word32
+peekFileAttributes p = peekByteOff p index_WIN32_FILE_ATTRIBUTE_DATA_dwFileAttributes
+    where index_WIN32_FILE_ATTRIBUTE_DATA_dwFileAttributes = 0
+
 peekLastWriteTimeLow :: Ptr WIN32_FILE_ATTRIBUTE_DATA -> IO Word32
 peekLastWriteTimeLow p = peekByteOff p index_WIN32_FILE_ATTRIBUTE_DATA_ftLastWriteTime_dwLowDateTime
     where index_WIN32_FILE_ATTRIBUTE_DATA_ftLastWriteTime_dwLowDateTime = 20
@@ -122,9 +149,12 @@
 
 #else
 -- Unix version
-getFileInfo x = handleJust (\e -> if isDoesNotExistError e then Just () else Nothing) (const $ return Nothing) $ do
+getFileInfo x = handleBool isDoesNotExistError (const $ return Nothing) $ do
     s <- getFileStatus $ unpackU_ x
-    return $ Just (fileInfo $ extractFileTime s, fileInfo $ fromIntegral $ fileSize s)
+    if isDirectory s then
+        errorIO $ "getFileInfo, expected a file, got a directory: " ++ unpackU x
+     else
+        result (extractFileTime s) (fromIntegral $ fileSize s)
 
 extractFileTime :: FileStatus -> Word32
 #ifndef MIN_VERSION_unix
diff --git a/src/Development/Shake/FilePath.hs b/src/Development/Shake/FilePath.hs
--- a/src/Development/Shake/FilePath.hs
+++ b/src/Development/Shake/FilePath.hs
@@ -71,17 +71,17 @@
 takeDirectory1 = takeWhile (not . isPathSeparator)
 
 
--- | Normalise a 'FilePath', trying to do:
+-- | Normalise a 'FilePath', applying the rules:
 --
--- * All 'pathSeparators' become @\/@
+-- * All 'pathSeparators' become 'pathSeparator' (@\/@ on Linux, @\\@ on Windows)
 --
--- * @foo\/bar\/..\/baz@ becomes @foo\/baz@
+-- * @foo\/bar\/..\/baz@ becomes @foo\/baz@ (not universally true in the presence of symlinks)
 --
 -- * @foo\/.\/bar@ becomes @foo\/bar@
 --
 -- * @foo\/\/bar@ becomes @foo\/bar@
 --
---   This function is not based on the normalise function from the filepath library, as that function
+--   This function is not based on the 'normalise' function from the @filepath@ library, as that function
 --   is quite broken.
 normaliseEx :: FilePath -> FilePath
 normaliseEx xs | a:b:xs <- xs, isWindows && sep a && sep b = '/' : f ('/':xs) -- account for UNC paths being double //
@@ -107,7 +107,7 @@
         g i (x:xs) = g (i-1) xs
 
         split xs = if null ys then [] else a : split b
-            where (a,b) = break sep $ ys
+            where (a,b) = break sep ys
                   ys = dropWhile sep xs
 
 
diff --git a/src/Development/Shake/FilePattern.hs b/src/Development/Shake/FilePattern.hs
--- a/src/Development/Shake/FilePattern.hs
+++ b/src/Development/Shake/FilePattern.hs
@@ -1,19 +1,30 @@
 {-# LANGUAGE PatternGuards, ViewPatterns #-}
 
 module Development.Shake.FilePattern(
+    -- * Primitive API, as exposed
     FilePattern, (?==), (<//>),
-    compatible, simple, extract, substitute,
-    directories, directories1
+    -- * Optimisation opportunities
+    simple,
+    -- * Multipattern file rules
+    compatible, extract, substitute,
+    -- * Accelerated searching
+    Walk(..), walk,
+    -- * Testing only
+    internalTest, isRelativePath, isRelativePattern
     ) where
 
-import System.FilePath(isPathSeparator, pathSeparators, pathSeparator)
+import Development.Shake.Errors
+import System.FilePath(isPathSeparator)
 import Data.List.Extra
+import Control.Applicative
+import Control.Monad
+import Data.Char
 import Data.Tuple.Extra
+import Data.Maybe
+import System.Info.Extra
+import Prelude
 
 
----------------------------------------------------------------------
--- BASIC FILE PATTERN MATCHING
-
 -- | A type synonym for file patterns, containing @\/\/@ and @*@. For the syntax
 --   and semantics of 'FilePattern' see '?=='.
 --
@@ -31,61 +42,163 @@
 --   then the second argument will be returned.
 type FilePattern = String
 
+infixr 5 <//>
 
-data Lexeme = Star | SlashSlash | Char Char deriving (Show, Eq)
+-- | Join two 'FilePattern' values by inserting two @\/@ characters between them.
+--   Will first remove any trailing path separators on the first argument, and any leading
+--   separators on the second.
+--
+-- > "dir" <//> "*" == "dir//*"
+(<//>) :: FilePattern -> FilePattern -> FilePattern
+a <//> b = dropWhileEnd isPathSeparator a ++ "//" ++ dropWhile isPathSeparator b
 
-isChar (Char _) = True; isChar _ = False
-isDull (Char x) = not $ isPathSeparator x; isDull _ = False
-fromChar (Char x) = x
 
+---------------------------------------------------------------------
+-- PATTERNS
 
-data Regex = Lit [Char] | Not [Char] | Any
-           | Start | End
-           | Bracket Regex
-           | Or Regex Regex | Concat Regex Regex
-           | Repeat Regex | Empty
-             deriving Show
+data Pat = Lit String -- ^ foo
+         | Star   -- ^ /*/
+         | Skip -- ^ //
+         | Skip1 -- ^ //, but must be at least 1 element
+         | Stars String [String] String -- ^ *foo*, prefix (fixed), infix floaters, suffix
+                                        -- e.g. *foo*bar = Stars "" ["foo"] "bar"
+            deriving (Show,Eq,Ord)
 
-type SString = (Bool, String) -- fst is True if at the start of the string
+isLit Lit{} = True; isLit _ = False
+fromLit (Lit x) = x
 
 
+data Lexeme = Str String | Slash | SlashSlash
+
 lexer :: FilePattern -> [Lexeme]
-lexer ('*':xs) = Star : lexer xs
-lexer (s1:s2:xs) | isPathSeparator s1 && isPathSeparator s2 = SlashSlash : lexer xs
-lexer (x:xs) = Char x : lexer xs
-lexer [] = []
+lexer "" = []
+lexer (x1:x2:xs) | isPathSeparator x1, isPathSeparator x2 = SlashSlash : lexer xs
+lexer (x1:xs) | isPathSeparator x1 = Slash : lexer xs
+lexer xs = Str a : lexer b
+    where (a,b) = break isPathSeparator xs
 
 
-pattern :: [Lexeme] -> Regex
-pattern = Concat Start . foldr Concat End . map f
+-- | Parse a FilePattern. All optimisations I can think of are invalid because they change the extracted expressions.
+parse :: FilePattern -> [Pat]
+parse = f False True . lexer
     where
-        f Star = Bracket $ Repeat $ Not pathSeparators
-        f SlashSlash = let s = Start `Or` End `Or` Lit pathSeparators in Bracket $
-                       Or (s `Concat` Repeat Any `Concat` s) (Lit pathSeparators)
-        f (Char x) = Lit $ if isPathSeparator x then pathSeparators else [x]
+        -- str = I have ever seen a Str go past (equivalent to "can I be satisfied by no paths")
+        -- slash = I am either at the start, or my previous character was Slash
+        f str slash [] = [Lit "" | slash]
+        f str slash (Str "**":xs) = Skip : f True False xs
+        f str slash (Str x:xs) = parseLit x : f True False xs
+        f str slash (SlashSlash:Slash:xs) | not str = Skip1 : f str True xs
+        f str slash (SlashSlash:xs) = Skip : f str False xs
+        f str slash (Slash:xs) = [Lit "" | not str] ++ f str True xs
 
 
--- | Return is (brackets, matched, rest)
-match :: Regex -> SString -> [([String], String, SString)]
-match (Lit l) (_, x:xs) | x `elem` l = [([], [x], (False, xs))]
-match (Not l) (_, x:xs) | x `notElem` l = [([], [x], (False, xs))]
-match Any (_, x:xs) = [([], [x], (False, xs))]
-match Start (True, xs) = [([], [], (True, xs))]
-match End (s, []) = [([], [], (s, []))]
-match (Bracket r) xs = [(a ++ [b], b, c) | (a,b,c) <- match r xs]
-match (Or r1 r2) xs = match r1 xs ++ match r2 xs
-match (Concat r1 r2) xs = [(a1++a2,b1++b2,c2) | (a1,b1,c1) <- match r1 xs, (a2,b2,c2) <- match r2 c1]
-match (Repeat r) xs = match (Empty `Or` Concat r (Repeat r)) xs
-match Empty xs = [([], "", xs)]
+parseLit :: String -> Pat
+parseLit "*" = Star
+parseLit x = case split (== '*') x of
+    [x] -> Lit x
+    pre:xs | Just (mid,post) <- unsnoc xs -> Stars pre mid post
+
+
+internalTest :: IO ()
+internalTest = do
+    let x # y = when (parse x /= y) $ fail $ show ("FilePattern.internalTest",x,parse x,y)
+    "" # [Lit ""]
+    "x" # [Lit "x"]
+    "/" # [Lit "",Lit ""]
+    "x/" # [Lit "x",Lit ""]
+    "/x" # [Lit "",Lit "x"]
+    "x/y" # [Lit "x",Lit "y"]
+    "//" # [Skip]
+    "**" # [Skip]
+    "//x" # [Skip, Lit "x"]
+    "**/x" # [Skip, Lit "x"]
+    "x//" # [Lit "x", Skip]
+    "x/**" # [Lit "x", Skip]
+    "x//y" # [Lit "x",Skip, Lit "y"]
+    "x/**/y" # [Lit "x",Skip, Lit "y"]
+    "///" # [Skip1, Lit ""]
+    "**/**" # [Skip,Skip]
+    "**/**/" # [Skip, Skip, Lit ""]
+    "///x" # [Skip1, Lit "x"]
+    "**/x" # [Skip, Lit "x"]
+    "x///" # [Lit "x", Skip, Lit ""]
+    "x/**/" # [Lit "x", Skip, Lit ""]
+    "x///y" # [Lit "x",Skip, Lit "y"]
+    "x/**/y" # [Lit "x",Skip, Lit "y"]
+    "////" # [Skip, Skip]
+    "**/**/**" # [Skip, Skip, Skip]
+    "////x" # [Skip, Skip, Lit "x"]
+    "x////" # [Lit "x", Skip, Skip]
+    "x////y" # [Lit "x",Skip, Skip, Lit "y"]
+    "**//x" # [Skip, Skip, Lit "x"]
+
+
+-- | Optimisations that may change the matched expressions
+optimise :: [Pat] -> [Pat]
+optimise (Skip:Skip:xs) = optimise $ Skip:xs
+optimise (Skip:Star:xs) = optimise $ Skip1:xs
+optimise (Star:Skip:xs) = optimise $ Skip1:xs
+optimise (x:xs) = x : optimise xs
+optimise [] =[]
+
+
+-- | A 'FilePattern' that will only match 'isRelativePath' values.
+isRelativePattern :: FilePattern -> Bool
+isRelativePattern ('*':'*':xs)
+    | [] <- xs = True
+    | x:xs <- xs, isPathSeparator x = True
+isRelativePattern _ = False
+
+-- | A non-absolute 'FilePath'.
+isRelativePath :: FilePath -> Bool
+isRelativePath (x:_) | isPathSeparator x = False
+isRelativePath (x:':':_) | isWindows, isAlpha x = False
+isRelativePath _ = True
+
+
+-- | Given a pattern, and a list of path components, return a list of all matches
+--   (for each wildcard in order, what the wildcard matched).
+match :: [Pat] -> [String] -> [[String]]
+match (Skip:xs) (y:ys) = map ("":) (match xs (y:ys)) ++ match (Skip1:xs) (y:ys)
+match (Skip1:xs) (y:ys) = [(y++"/"++r):rs | r:rs <- match (Skip:xs) ys]
+match (Skip:xs) [] = map ("":) $ match xs []
+match (Star:xs) (y:ys) = map (y:) $ match xs ys
+match (Lit x:xs) (y:ys) | x == y = match xs ys
+match (x@Stars{}:xs) (y:ys) | Just rs <- matchStars x y = map (rs ++) $ match xs ys
+match [] [] = [[]]
 match _ _ = []
 
 
--- | Match a 'FilePattern' against a 'FilePath', There are only two special forms:
+matchOne :: Pat -> String -> Bool
+matchOne (Lit x) y = x == y
+matchOne x@Stars{} y = isJust $ matchStars x y
+matchOne Star _ = True
+
+
+-- Only return the first (all patterns left-most) valid star matching
+matchStars :: Pat -> String -> Maybe [String]
+matchStars (Stars pre mid post) x = do
+    x <- stripPrefix pre x
+    x <- if null post then Just x else stripSuffix post x
+    stripInfixes mid x
+    where
+        stripInfixes [] x = Just [x]
+        stripInfixes (m:ms) x = do
+            (a,x) <- stripInfix m x
+            (a:) <$> stripInfixes ms x
+
+
+-- | Match a 'FilePattern' against a 'FilePath', There are three special forms:
 --
 -- * @*@ matches an entire path component, excluding any separators.
 --
--- * @\/\/@ matches an arbitrary number of path components.
+-- * @\/\/@ matches an arbitrary number of path components, including absolute path
+--   prefixes.
 --
+-- * @**@ as a path component matches an arbitrary number of path components, but not
+--   absolute path prefixes.
+--   Currently considered experimental.
+--
 --   Some examples:
 --
 -- * @test.c@ matches @test.c@ and nothing else.
@@ -93,9 +206,9 @@
 -- * @*.c@ matches all @.c@ files in the current directory, so @file.c@ matches,
 --   but @file.h@ and @dir\/file.c@ don't.
 --
--- * @\/\/*.c@ matches all @.c@ files in the current directory or its subdirectories,
---   so @file.c@, @dir\/file.c@ and @dir1\/dir2\/file.c@ all match, but @file.h@ and
---   @dir\/file.h@ don't.
+-- * @\/\/*.c@ matches all @.c@ files anywhere on the filesystem,
+---  so @file.c@, @dir\/file.c@, @dir1\/dir2\/file.c@ and @/path/to/file.c@ all match,
+--   but @file.h@ and @dir\/file.h@ don't.
 --
 -- * @dir\/*\/*@ matches all files one level below @dir@, so @dir\/one\/file.c@ and
 --   @dir\/two\/file.h@ match, but @file.c@, @one\/dir\/file.c@, @dir\/file.h@
@@ -104,89 +217,93 @@
 --   Patterns with constructs such as @foo\/..\/bar@ will never match
 --   normalised 'FilePath' values, so are unlikely to be correct.
 (?==) :: FilePattern -> FilePath -> Bool
-(?==) [s1,s2,'*'] | isPathSeparator s1 && isPathSeparator s2 = const True
-(?==) p = \x -> not $ null $ match pat (True, x)
-    where pat = pattern $ lexer p
-
-infixr 5 <//>
-
--- | Join two 'FilePattern' values by inserting two @\/@ characters between them.
---   Will first remove any trailing path separators on the first argument, and any leading
---   separators on the second.
---
--- > "dir" <//> "*" == "dir//*"
-(<//>) :: FilePattern -> FilePattern -> FilePattern
-a <//> b = dropWhileEnd isPathSeparator a ++ "//" ++ dropWhile isPathSeparator b
+(?==) p = case optimise $ parse p of
+    [x] | x == Skip || x == Skip1 -> if rp then isRelativePath else const True
+    p -> let f = not . null . match p . split isPathSeparator
+         in if rp then (\x -> isRelativePath x && f x) else f
+    where rp = isRelativePattern p
 
 
 ---------------------------------------------------------------------
--- DIRECTORY PATTERNS
-
--- | Given a pattern, return the directory that requires searching,
---   with 'True' if it requires a recursive search. Must be conservative.
---   Examples:
---
--- > directories1 "*.xml" == ("",False)
--- > directories1 "//*.xml" == ("",True)
--- > directories1 "foo//*.xml" == ("foo",True)
--- > directories1 "foo/bar/*.xml" == ("foo/bar",False)
--- > directories1 "*/bar/*.xml" == ("",True)
-directories1 :: FilePattern -> (FilePath, Bool)
-directories1 = first (intercalate [pathSeparator]) . f . lexer
-    where
-        f xs | (a@(_:_),b:bs) <- span isDull xs, b `elem` (SlashSlash:map Char pathSeparators) =
-                if b == SlashSlash then ([map fromChar a],True) else first (map fromChar a:) $ f bs
-             | all (\x -> isDull x || x == Star) xs = ([],False)
-             | otherwise = ([], True)
-
+-- MULTIPATTERN COMPATIBLE SUBSTITUTIONS
 
--- | Given a set of patterns, produce a set of directories that require searching,
---   with 'True' if it requires a recursive search. Must be conservative. Examples:
---
--- > directories ["*.xml","//*.c"] == [("",True)]
--- > directories ["bar/*.xml","baz//*.c"] == [("bar",False),("baz",True)]
--- > directories ["bar/*.xml","baz//*.c"] == [("bar",False),("baz",True)]
-directories :: [FilePattern] -> [(FilePath,Bool)]
-directories ps = foldl f xs xs
+specials :: FilePattern -> [Pat]
+specials = concatMap f . parse
     where
-        xs = nubOrd $ map directories1 ps
-
-        -- Eliminate anything which is a strict subset
-        f xs (x,True) = filter (\y -> not $ (x,False) == y || x `isPrefixSlashOf` fst y) xs
-        f xs _ = xs
-
-        isPrefixSlashOf x (stripPrefix x -> Just (s1:_)) = isPathSeparator s1
-        isPrefixSlashOf _ _ = False
-
-
----------------------------------------------------------------------
--- MULTIPATTERN COMPATIBLE SUBSTITUTIONS
+        f Lit{} = []
+        f Star = [Star]
+        f Skip = [Skip]
+        f Skip1 = [Skip]
+        f (Stars _ xs _) = replicate (length xs + 1) Star
 
 -- | Is the pattern free from any * and //.
 simple :: FilePattern -> Bool
-simple = all isChar . lexer
-
+simple = null . specials
 
 -- | Do they have the same * and // counts in the same order
 compatible :: [FilePattern] -> Bool
 compatible [] = True
-compatible (x:xs) = all ((==) (f x) . f) xs
-    where f = filter (not . isChar) . lexer
-
+compatible (x:xs) = all ((==) (specials x) . specials) xs
 
 -- | Extract the items that match the wildcards. The pair must match with '?=='.
 extract :: FilePattern -> FilePath -> [String]
-extract p x = ms
-    where (ms,_,_):_ = match (pattern $ lexer p) (True,x)
+extract p = let pat = parse p in \x ->
+    case match pat (split isPathSeparator x) of
+        [] | p ?== x -> err $ "extract with " ++ show p ++ " and " ++ show x
+           | otherwise -> error $ "Pattern " ++ show p ++ " does not match " ++ x ++ ", when trying to extract the FilePattern matches"
+        ms:_ -> ms
 
 
 -- | Given the result of 'extract', substitute it back in to a 'compatible' pattern.
 --
 -- > p '?==' x ==> substitute (extract p x) p == x
 substitute :: [String] -> FilePattern -> FilePath
-substitute ms p = f ms (lexer p)
+substitute oms oxs = intercalate "/" $ concat $ snd $ mapAccumL f oms (parse oxs)
     where
-        f ms (Char p:ps) = p : f ms ps
-        f (m:ms) (_:ps) = m ++ f ms ps
-        f [] [] = []
-        f _ _ = error $ "Substitution failed into pattern " ++ show p ++ " with " ++ show (length ms) ++ " matches, namely " ++ show ms
+        f ms (Lit x) = (ms, [x])
+        f (m:ms) Star = (ms, [m])
+        f (m:ms) Skip = (ms, split m)
+        f (m:ms) Skip1 = (ms, split m)
+        f ms (Stars pre mid post) = (ms2, [concat $ pre : zipWith (++) ms1 (mid++[post])])
+            where (ms1,ms2) = splitAt (length mid + 1) ms
+        f _ _ = error $ "Substitution failed into pattern " ++ show oxs ++ " with " ++ show (length oms) ++ " matches, namely " ++ show oms
+
+        split = linesBy (== '/')
+
+
+---------------------------------------------------------------------
+-- EFFICIENT PATH WALKING
+
+-- | Given a list of files, return a list of things I can match in this directory
+--   plus a list of subdirectories and walks that apply to them.
+--   Use WalkTo when the list can be predicted in advance
+data Walk = Walk ([String] -> ([String],[(String,Walk)]))
+          | WalkTo            ([String],[(String,Walk)])
+
+walk :: [FilePattern] -> (Bool, Walk)
+walk ps = let ps2 = map (optimise . parse) ps in (any (\p -> isEmpty p || not (null $ match p [""])) ps2, f ps2)
+    where
+        f (nubOrd -> ps)
+            | all isLit fin, all (isLit . fst) nxt = WalkTo (map fromLit fin, map (fromLit *** f) nxt)
+            | otherwise = Walk $ \xs ->
+                (if finStar then xs else filter (\x -> any (`matchOne` x) fin) xs
+                ,[(x, f ys) | x <- xs, let ys = concat [b | (a,b) <- nxt, matchOne a x], not $ null ys])
+            where
+                finStar = Star `elem` fin
+                fin = nubOrd $ mapMaybe final ps
+                nxt = groupSort $ concatMap next ps
+
+
+next :: [Pat] -> [(Pat, [Pat])]
+next (Skip1:xs) = [(Star,Skip:xs)]
+next (Skip:xs) = (Star,Skip:xs) : next xs
+next (x:xs) = [(x,xs) | not $ null xs]
+next [] = []
+
+final :: [Pat] -> Maybe Pat
+final (Skip:xs) = if isEmpty xs then Just Star else final xs
+final (Skip1:xs) = if isEmpty xs then Just Star else Nothing
+final (x:xs) = if isEmpty xs then Just x else Nothing
+final [] = Nothing
+
+isEmpty = all (== Skip)
diff --git a/src/Development/Shake/Forward.hs b/src/Development/Shake/Forward.hs
new file mode 100644
--- /dev/null
+++ b/src/Development/Shake/Forward.hs
@@ -0,0 +1,108 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable, Rank2Types, ScopedTypeVariables, MultiParamTypeClasses #-}
+
+-- | A module for producing forward-defined build systems, in contrast to standard backwards-defined
+--   build systems such as shake. Based around ideas from <https://code.google.com/p/fabricate/ fabricate>.
+--   As an example:
+--
+-- @
+-- import "Development.Shake"
+-- import "Development.Shake.Forward"
+-- import "Development.Shake.FilePath"
+--
+-- main = 'shakeArgsForward' 'shakeOptions' $ do
+--     contents <- 'readFileLines' \"result.txt\"
+--     'cache' $ 'cmd' \"tar -cf result.tar\" contents
+-- @
+--
+--   Compared to backward-defined build systems (such as normal Shake), forward-defined build
+--   systems tend to be simpler for simple systems (less boilerplate, more direct style), but more
+--   complex for larger build systems (requires explicit parallelism, explicit sharing of build products,
+--   no automatic command line targets). As a general approach for writing forward-defined systems:
+--
+-- * Figure out the sequence of system commands that will build your project.
+--
+-- * Write a simple 'Action' that builds your project.
+--
+-- * Insert 'cache' in front of most system commands.
+--
+-- * Replace most loops with 'forP', where they can be executed in parallel.
+--
+-- * Where Haskell performs real computation, if zero-build performance is insufficient, use 'cacheAction'.
+--
+--   All forward-defined systems use 'AutoDeps', which requires @fsatrace@ to be on the @$PATH@.
+--   You can obtain @fsatrace@ from <https://github.com/jacereda/fsatrace>.
+module Development.Shake.Forward(
+    shakeForward, shakeArgsForward,
+    forwardOptions, forwardRule,
+    cache, cacheAction
+    ) where
+
+import Development.Shake
+import Development.Shake.Rule
+import Development.Shake.Command
+import Development.Shake.Classes
+import Development.Shake.FilePath
+import Data.IORef
+import Data.Either
+import Data.List.Extra
+import Control.Exception.Extra
+import Numeric
+import System.IO.Unsafe
+import qualified Data.HashMap.Strict as Map
+
+
+{-# NOINLINE forwards #-}
+forwards :: IORef (Map.HashMap ForwardQ (Action ()))
+forwards = unsafePerformIO $ newIORef Map.empty
+
+newtype ForwardQ = ForwardQ String
+    deriving (Hashable,Typeable,Eq,NFData,Binary)
+
+instance Show ForwardQ where
+    show (ForwardQ x) = x
+
+newtype ForwardA = ForwardA ()
+    deriving (Hashable,Typeable,Eq,NFData,Binary,Show)
+
+instance Rule ForwardQ ForwardA where
+    storedValue _ _ = return $ Just $ ForwardA ()
+
+-- | Run a forward-defined build system.
+shakeForward :: ShakeOptions -> Action () -> IO ()
+shakeForward opts act = shake (forwardOptions opts) (forwardRule act)
+
+-- | Run a forward-defined build system, interpretting command-line arguments.
+shakeArgsForward :: ShakeOptions -> Action () -> IO ()
+shakeArgsForward opts act = shakeArgs (forwardOptions opts) (forwardRule act)
+
+-- | Given an 'Action', turn it into a 'Rules' structure which runs in forward mode.
+forwardRule :: Action () -> Rules ()
+forwardRule act = do
+    rule $ \k -> Just $ do
+        res <- liftIO $ atomicModifyIORef forwards $ \mp -> (Map.delete k mp, Map.lookup k mp)
+        case res of
+            Nothing -> liftIO $ errorIO "Failed to find action name"
+            Just act -> act
+        return $ ForwardA ()
+    action act
+
+-- | Given a 'ShakeOptions', set the options necessary to execute in forward mode.
+forwardOptions :: ShakeOptions -> ShakeOptions
+forwardOptions opts = opts{shakeCommandOptions=[AutoDeps]}
+
+
+-- | Cache an action. The name of the action must be unique for all different actions.
+cacheAction :: String -> Action () -> Action ()
+cacheAction name action = do
+    let key = ForwardQ name
+    liftIO $ atomicModifyIORef forwards $ \mp -> (Map.insert key action mp, ())
+    _ :: [ForwardA] <- apply [key]
+    liftIO $ atomicModifyIORef forwards $ \mp -> (Map.delete key mp, ())
+
+-- | Apply caching to an external command.
+cache :: (forall r . CmdArguments r => r) -> Action ()
+cache cmd = do
+    let args :: [Either CmdOption String] = cmd
+    let isDull ['-',x] = True; isDull _ = False
+    let name = head $ filter (not . isDull) (drop 1 $ rights args) ++ ["unknown"]
+    cacheAction ("command " ++ toStandard name ++ " #" ++ upper (showHex (abs $ hash $ show args) "")) cmd
diff --git a/src/Development/Shake/Monad.hs b/src/Development/Shake/Monad.hs
--- a/src/Development/Shake/Monad.hs
+++ b/src/Development/Shake/Monad.hs
@@ -14,6 +14,7 @@
 import Control.Monad.Trans.Reader
 import Data.IORef
 import Control.Applicative
+import Control.Monad
 import Prelude
 
 
@@ -29,13 +30,18 @@
 type Capture a = (a -> IO ()) -> IO ()
 
 
+-- See https://ghc.haskell.org/trac/ghc/ticket/11555
+catchSafe :: IO a -> (SomeException -> IO a) -> IO a
+catchSafe a b = join (evaluate a) `catch_` b
+
 -- | Run and then call a continuation.
 runRAW :: ro -> rw -> RAW ro rw a -> Capture (Either SomeException a)
 runRAW ro rw m k = do
     rww <- newIORef rw
     handler <- newIORef $ k . Left
+    -- see https://ghc.haskell.org/trac/ghc/ticket/11555
     fromRAW m `runReaderT` S handler ro rww `runContT` (k . Right)
-        `catch_` \e -> ($ e) =<< readIORef handler
+        `catchSafe` \e -> ($ e) =<< readIORef handler
 
 
 ---------------------------------------------------------------------
@@ -81,7 +87,7 @@
     old <- readIORef $ handler s
     writeIORef (handler s) $ \e -> do
         writeIORef (handler s) old
-        fromRAW (hdl e) `runReaderT` s `runContT` k `catch_`
+        fromRAW (hdl e) `runReaderT` s `runContT` k `catchSafe`
             \e -> ($ e) =<< readIORef (handler s)
     fromRAW m `runReaderT` s `runContT` \v -> do
         writeIORef (handler s) old
@@ -118,5 +124,5 @@
         Left e -> old e
         Right v -> do
             writeIORef (handler s) old
-            k v `catch_` \e -> ($ e) =<< readIORef (handler s)
+            k v `catchSafe` \e -> ($ e) =<< readIORef (handler s)
             writeIORef (handler s) throwIO
diff --git a/src/Development/Shake/Pool.hs b/src/Development/Shake/Pool.hs
--- a/src/Development/Shake/Pool.hs
+++ b/src/Development/Shake/Pool.hs
@@ -7,11 +7,13 @@
     ) where
 
 import Control.Concurrent.Extra
+import Development.Shake.Errors
+import System.Time.Extra
 import Control.Exception
 import Control.Monad
 import General.Timing
 import qualified Data.HashSet as Set
-import System.IO.Unsafe
+import qualified Data.HashMap.Strict as Map
 import System.Random
 
 
@@ -21,11 +23,6 @@
 -- Monad for non-deterministic (but otherwise pure) computations
 type NonDet a = IO a
 
-nonDet :: NonDet [Bool]
-nonDet = do bs <- unsafeInterleaveIO nonDet
-            b <- randomIO
-            return $ b:bs
-
 -- Left = deterministic list, Right = non-deterministic tree
 data Queue a = Queue [a] (Either [a] (Maybe (Tree a)))
 
@@ -35,38 +32,42 @@
 enqueuePriority :: a -> Queue a -> Queue a
 enqueuePriority x (Queue p t) = Queue (x:p) t
 
-enqueue :: a -> Queue a -> NonDet (Queue a)
-enqueue x (Queue p (Left xs)) = return $ Queue p $ Left $ x:xs
-enqueue x (Queue p (Right Nothing)) = return $ Queue p $ Right $ Just $ Leaf x
-enqueue x (Queue p (Right (Just t))) = do bs <- nonDet; return $ Queue p $ Right $ Just $ insertTree bs x t
+enqueue :: a -> Queue a -> Queue a
+enqueue x (Queue p (Left xs)) = Queue p $ Left $ x:xs
+enqueue x (Queue p (Right Nothing)) = Queue p $ Right $ Just $ singleTree x
+enqueue x (Queue p (Right (Just t))) = Queue p $ Right $ Just $ insertTree x t
 
 dequeue :: Queue a -> Maybe (NonDet (a, Queue a))
 dequeue (Queue (p:ps) t) = Just $ return (p, Queue ps t)
 dequeue (Queue [] (Left (x:xs))) = Just $ return (x, Queue [] $ Left xs)
 dequeue (Queue [] (Left [])) = Nothing
-dequeue (Queue [] (Right (Just t))) = Just $ do bs <- nonDet; (x,t) <- return $ removeTree bs t; return (x, Queue [] $ Right t)
+dequeue (Queue [] (Right (Just t))) = Just $ do bs <- randomIO; (x,t) <- return $ removeTree bs t; return (x, Queue [] $ Right t)
 dequeue (Queue [] (Right Nothing)) = Nothing
 
 
 ---------------------------------------------------------------------
 -- TREE
 
--- Note that for a Random tree, since everything is Random, Branch x y =~= Branch y x
-data Tree a = Leaf a | Branch (Tree a) (Tree a)
+-- A tree where removal is random. Nodes are stored at indicies 0..n-1
+data Tree a = Tree {-# UNPACK #-} !Int (Map.HashMap Int a)
 
-insertTree :: [Bool] -> a -> Tree a -> Tree a
-insertTree _ x (Leaf y) = Branch (Leaf x) (Leaf y)
-insertTree (b:bs) x (Branch y z) = if b then f y z else f z y
-    where f y z = Branch y (insertTree bs x z)
+singleTree :: a -> Tree a
+singleTree x = Tree 1 $ Map.singleton 0 x
 
-removeTree :: [Bool] -> Tree a -> (a, Maybe (Tree a))
-removeTree _ (Leaf x) = (x, Nothing)
-removeTree (b:bs) (Branch y z) = if b then f y z else f z y
+insertTree :: a -> Tree a -> Tree a
+insertTree x (Tree n mp) = Tree (n+1) $ Map.insert n x mp
+
+-- Remove an item at random, put the n-1 item to go in it's place
+removeTree :: Int -> Tree a -> (a, Maybe (Tree a))
+removeTree rnd (Tree n mp)
+        | n == 0 = err "removeTree, tree is empty"
+        | n == 1 = (mp Map.! 0, Nothing)
+        | i == n-1 = (mp Map.! i, Just $ Tree (n-1) $ Map.delete i mp)
+        | otherwise = (mp Map.! i, Just $ Tree (n-1) $ Map.insert i (mp Map.! (n-1)) $ Map.delete (n-1) mp)
     where
-        f y z = case removeTree bs z of
-                    (x, Nothing) -> (x, Just y)
-                    (x, Just z) -> (x, Just $ Branch y z)
+        i = abs rnd `mod` n
 
+
 ---------------------------------------------------------------------
 -- THREAD POOL
 
@@ -92,6 +93,16 @@
 emptyS n deterministic = S Set.empty n 0 0 $ newQueue deterministic
 
 
+worker :: Pool -> IO ()
+worker pool@(Pool var done) = do
+    let onVar act = modifyVar var $ maybe (return (Nothing, return ())) act
+    join $ onVar $ \s -> do
+        res <- maybe (return Nothing) (fmap Just) $ dequeue $ todo s
+        case res of
+            Nothing -> return (Just s, return ())
+            Just (now, todo2) -> return (Just s{todo = todo2}, now >> worker pool)
+
+
 -- | Given a pool, and a function that breaks the S invariants, restore them
 --   They are only allowed to touch threadsLimit or todo
 step :: Pool -> (S -> NonDet S) -> IO ()
@@ -103,15 +114,15 @@
         case res of
             Just (now, todo2) | Set.size (threads s) < threadsLimit s -> do
                 -- spawn a new worker
-                t <- forkIO $ do
-                    t <- myThreadId
-                    res <- try now
-                    case res of
-                        Left e -> onVar $ \s -> do
-                            mapM_ killThread $ Set.toList $ Set.delete t $ threads s
-                            signalBarrier done $ Left e
-                            return Nothing
-                        Right _ -> step pool $ \s -> return s{threads = Set.delete t $ threads s}
+                t <- forkFinally (now >> worker pool) $ \res -> case res of
+                    Left e -> onVar $ \s -> do
+                        t <- myThreadId
+                        mapM_ killThread $ Set.toList $ Set.delete t $ threads s
+                        signalBarrier done $ Left e
+                        return Nothing
+                    Right _ -> do
+                        t <- myThreadId
+                        step pool $ \s -> return s{threads = Set.delete t $ threads s}
                 let threads2 = Set.insert t $ threads s
                 return $ Just s{todo = todo2, threads = threads2
                                ,threadsSum = threadsSum s + 1, threadsMax = threadsMax s `max` Set.size threads2}
@@ -124,7 +135,7 @@
 -- | Add a new task to the pool, may be cancelled by sending it an exception
 addPool :: Pool -> IO a -> IO ()
 addPool pool act = step pool $ \s -> do
-    todo <- enqueue (void act) (todo s)
+    todo <- return $ enqueue (void act) (todo s)
     return s{todo = todo}
 
 -- | Add a new task to the pool, may be cancelled by sending it an exception.
@@ -148,17 +159,29 @@
 runPool :: Bool -> Int -> (Pool -> IO ()) -> IO () -- run all tasks in the pool
 runPool deterministic n act = do
     s <- newVar $ Just $ emptyS n deterministic
+    done <- newBarrier
+
     let cleanup = modifyVar_ s $ \s -> do
             -- if someone kills our thread, make sure we kill our child threads
             case s of
                 Just s -> mapM_ killThread $ Set.toList $ threads s
                 Nothing -> return ()
             return Nothing
-    flip onException cleanup $ do
-        res <- newBarrier
-        let pool = Pool s res
+
+    let ghc10793 = do
+            -- if this thread dies because it is blocked on an MVar there's a chance we have
+            -- a better error in the done barrier, and GHC raised the exception wrongly, see:
+            -- https://ghc.haskell.org/trac/ghc/ticket/10793
+            sleep 1 -- give it a little bit of time for the finally to run
+                    -- no big deal, since the blocked indefinitely takes a while to fire anyway
+            res <- waitBarrierMaybe done
+            case res of
+                Just (Left e) -> throwIO e
+                _ -> throwIO BlockedIndefinitelyOnMVar
+    handle (\BlockedIndefinitelyOnMVar -> ghc10793) $ flip onException cleanup $ do
+        let pool = Pool s done
         addPool pool $ act pool
-        res <- waitBarrier res
+        res <- waitBarrier done
         case res of
-            Left e -> throw e
+            Left e -> throwIO e
             Right s -> addTiming $ "Pool finished (" ++ show (threadsSum s) ++ " threads, " ++ show (threadsMax s) ++ " max)"
diff --git a/src/Development/Shake/Profile.hs b/src/Development/Shake/Profile.hs
--- a/src/Development/Shake/Profile.hs
+++ b/src/Development/Shake/Profile.hs
@@ -9,6 +9,7 @@
 import Data.Version
 import System.FilePath
 import Numeric.Extra
+import General.Extra
 import Paths_shake
 import System.Time.Extra
 import qualified Data.ByteString.Lazy.Char8 as LBS
@@ -38,13 +39,13 @@
     ,let f = show . sum . map (length . prfTraces) in "* Building required " ++ f xs ++ " traced commands (" ++ f ls ++ " in the last run)."
     ,"* The total (unparallelised) time is " ++ showDuration (sum $ map prfExecution xs) ++
         " of which " ++ showDuration (sum $ map prfTime $ concatMap prfTraces xs) ++ " is traced commands."
-    ,let f xs = if null xs then "0s" else (\(a,b) -> showDuration a ++ " (" ++ b ++ ")") $ maximumBy (compare `on` fst) xs in
+    ,let f xs = if null xs then "0s" else (\(a,b) -> showDuration a ++ " (" ++ b ++ ")") $ maximumBy' (compare `on` fst) xs in
         "* The longest rule takes " ++ f (map (prfExecution &&& prfName) xs) ++
         ", and the longest traced command takes " ++ f (map (prfTime &&& prfCommand) $ concatMap prfTraces xs) ++ "."
     ,let sumLast = sum $ map prfTime $ concatMap prfTraces ls
          maxStop = maximum $ 0 : map prfStop (concatMap prfTraces ls) in
         "* Last run gave an average parallelism of " ++ showDP 2 (if maxStop == 0 then 0 else sumLast / maxStop) ++
-        " times over " ++ showDuration(maxStop) ++ "."
+        " times over " ++ showDuration maxStop ++ "."
     ]
     where ls = filter ((==) 0 . prfBuilt) xs
 
@@ -53,7 +54,7 @@
 generateHTML xs = do
     htmlDir <- getDataFileName "html"
     report <- LBS.readFile $ htmlDir </> "profile.html"
-    let f name | name == "profile-data.js" = return $ LBS.pack $ "var shake =\n" ++ generateJSON xs
+    let f name | name == "profile-data.js" = return $ LBS.pack $ "var profile =\n" ++ generateJSON xs
                | name == "version.js" = return $ LBS.pack $ "var version = " ++ show (showVersion version)
                | otherwise = LBS.readFile $ htmlDir </> name
     runTemplate f report
@@ -81,11 +82,11 @@
             ,("built", show prfBuilt)
             ,("changed", show prfChanged)
             ,("depends", show prfDepends)
-            ,("execution", show prfExecution)] ++
+            ,("execution", showDP 4 prfExecution)] ++
             [("traces", jsonList $ map showTrace prfTraces) | not $ null prfTraces]
         showTrace ProfileTrace{..} = jsonObject
             [("command",show prfCommand), ("start",show prfStart), ("stop",show prfStop)]
 
 jsonListLines xs = "[" ++ intercalate "\n," xs ++ "\n]"
 jsonList xs = "[" ++ intercalate "," xs ++ "]"
-jsonObject xs = "{" ++ intercalate ", " [show a ++ ":" ++ b | (a,b) <- xs] ++ "}"
+jsonObject xs = "{" ++ intercalate "," [show a ++ ":" ++ b | (a,b) <- xs] ++ "}"
diff --git a/src/Development/Shake/Progress.hs b/src/Development/Shake/Progress.hs
--- a/src/Development/Shake/Progress.hs
+++ b/src/Development/Shake/Progress.hs
@@ -1,5 +1,4 @@
-{-# LANGUAGE DeriveDataTypeable, RecordWildCards, CPP, ForeignFunctionInterface #-}
-{-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE DeriveDataTypeable, RecordWildCards, CPP, ViewPatterns, ForeignFunctionInterface #-}
 
 -- | Progress tracking
 module Development.Shake.Progress(
@@ -37,9 +36,13 @@
 import Foreign
 import Foreign.C.Types
 
-type LPCSTR = Ptr CChar
+#ifdef x86_64_HOST_ARCH
+#define CALLCONV ccall
+#else
+#define CALLCONV stdcall
+#endif
 
-foreign import stdcall "Windows.h SetConsoleTitleA" c_setConsoleTitle :: LPCSTR -> IO Bool
+foreign import CALLCONV "Windows.h SetConsoleTitleA" c_setConsoleTitle :: Ptr CChar -> IO Bool
 
 #endif
 
@@ -47,9 +50,10 @@
 ---------------------------------------------------------------------
 -- PROGRESS TYPES - exposed to the user
 
--- | Information about the current state of the build, obtained by passing a callback function
---   to 'Development.Shake.shakeProgress'. Typically a program will use 'progressDisplay' to poll this value and produce
---   status messages, which is implemented using this data type.
+-- | Information about the current state of the build, obtained by either passing a callback function
+--   to 'Development.Shake.shakeProgress' (asynchronous output) or 'Development.Shake.getProgress'
+--   (synchronous output). Typically a build system will pass 'progressDisplay' to 'Development.Shake.shakeProgress',
+--   which will poll this value and produce status messages.
 data Progress = Progress
     {isFailure :: !(Maybe String) -- ^ Starts out 'Nothing', becomes 'Just' a target name if a rule fails.
     ,countSkipped :: {-# UNPACK #-} !Int -- ^ Number of rules which were required, but were already in a valid state.
@@ -274,7 +278,7 @@
 generateHTML xs = do
     htmlDir <- getDataFileName "html"
     report <- LBS.readFile $ htmlDir </> "progress.html"
-    let f name | name == "progress-data.js" = return $ LBS.pack $ "var shake =\n" ++ generateJSON xs
+    let f name | name == "progress-data.js" = return $ LBS.pack $ "var progress =\n" ++ generateJSON xs
                | name == "version.js" = return $ LBS.pack $ "var version = " ++ show (showVersion version)
                | otherwise = LBS.readFile $ htmlDir </> name
     runTemplate f report
@@ -298,9 +302,9 @@
 
 {-# NOINLINE xterm #-}
 xterm :: Bool
-xterm = System.IO.Unsafe.unsafePerformIO $
+xterm = unsafePerformIO $
     -- Terminal.app uses "xterm-256color" as its env variable
-    catch_ (fmap ("xterm" `isPrefixOf`) $ getEnv "TERM") $
+    catch_ (("xterm" `isPrefixOf`) <$> getEnv "TERM") $
     \e -> return False
 
 
@@ -344,7 +348,9 @@
                            in if null b then "" else reverse $ takeWhile isDigit $ reverse a
                 let key = (failure, perc)
                 same <- atomicModifyIORef ref $ \old -> (Just key, old == Just key)
-                let state = if perc == "" then "NoProgress" else if failure then "Error" else "Normal"
+                let state | perc == "" = "NoProgress"
+                          | failure = "Error"
+                          | otherwise = "Normal"
                 rawSystem exe $ ["--title=" ++ msg, "--state=" ++ state] ++ ["--value=" ++ perc | perc /= ""]
                 return ()
 
diff --git a/src/Development/Shake/Resource.hs b/src/Development/Shake/Resource.hs
--- a/src/Development/Shake/Resource.hs
+++ b/src/Development/Shake/Resource.hs
@@ -7,6 +7,7 @@
 import Data.Function
 import System.IO.Unsafe
 import Control.Concurrent.Extra
+import Control.Exception.Extra
 import Data.Tuple.Extra
 import Control.Monad
 import General.Bilist
@@ -71,7 +72,7 @@
 newResourceIO :: String -> Int -> IO Resource
 newResourceIO name mx = do
     when (mx < 0) $
-        error $ "You cannot create a resource named " ++ name ++ " with a negative quantity, you used " ++ show mx
+        errorIO $ "You cannot create a resource named " ++ name ++ " with a negative quantity, you used " ++ show mx
     key <- resourceId
     var <- newVar $ Finite mx mempty
     return $ Resource (negate key) shw (acquire var) (release var)
@@ -80,8 +81,8 @@
 
         acquire :: Var Finite -> Pool -> Int -> IO () -> IO ()
         acquire var pool want continue
-            | want < 0 = error $ "You cannot acquire a negative quantity of " ++ shw ++ ", requested " ++ show want
-            | want > mx = error $ "You cannot acquire more than " ++ show mx ++ " of " ++ shw ++ ", requested " ++ show want
+            | want < 0 = errorIO $ "You cannot acquire a negative quantity of " ++ shw ++ ", requested " ++ show want
+            | want > mx = errorIO $ "You cannot acquire more than " ++ show mx ++ " of " ++ shw ++ ", requested " ++ show want
             | otherwise = join  $ modifyVar var $ \x@Finite{..} -> return $
                 if want <= finiteAvailable then
                     (x{finiteAvailable = finiteAvailable - want}, continue)
@@ -131,7 +132,7 @@
 newThrottleIO :: String -> Int -> Double -> IO Resource
 newThrottleIO name count period = do
     when (count < 0) $
-        error $ "You cannot create a throttle named " ++ name ++ " with a negative quantity, you used " ++ show count
+        errorIO $ "You cannot create a throttle named " ++ name ++ " with a negative quantity, you used " ++ show count
     key <- resourceId
     var <- newVar $ ThrottleAvailable count
     return $ Resource key shw (acquire var) (release var)
@@ -140,8 +141,8 @@
 
         acquire :: Var Throttle -> Pool -> Int -> IO () -> IO ()
         acquire var pool want continue
-            | want < 0 = error $ "You cannot acquire a negative quantity of " ++ shw ++ ", requested " ++ show want
-            | want > count = error $ "You cannot acquire more than " ++ show count ++ " of " ++ shw ++ ", requested " ++ show want
+            | want < 0 = errorIO $ "You cannot acquire a negative quantity of " ++ shw ++ ", requested " ++ show want
+            | want > count = errorIO $ "You cannot acquire more than " ++ show count ++ " of " ++ shw ++ ", requested " ++ show want
             | otherwise = join $ modifyVar var $ \x -> case x of
                 ThrottleAvailable i
                     | i >= want -> return (ThrottleAvailable $ i - want, continue)
diff --git a/src/Development/Shake/Rule.hs b/src/Development/Shake/Rule.hs
--- a/src/Development/Shake/Rule.hs
+++ b/src/Development/Shake/Rule.hs
@@ -1,11 +1,7 @@
-{-# LANGUAGE CPP #-}
 
 -- | This module is used for defining new types of rules for Shake build systems.
 --   Most users will find the built-in set of rules sufficient.
 module Development.Shake.Rule(
-#if __GLASGOW_HASKELL__ >= 704
-    ShakeValue,
-#endif
     Rule(..), EqualCost(..), rule, apply, apply1,
     trackUse, trackChange, trackAllow,
     -- * Deprecated
diff --git a/src/Development/Shake/Rules/Directory.hs b/src/Development/Shake/Rules/Directory.hs
--- a/src/Development/Shake/Rules/Directory.hs
+++ b/src/Development/Shake/Rules/Directory.hs
@@ -6,15 +6,19 @@
     getDirectoryContents, getDirectoryFiles, getDirectoryDirs,
     getEnv, getEnvWithDefault,
     removeFiles, removeFilesAfter,
+    getDirectoryFilesIO,
     defaultRuleDirectory
     ) where
 
 import Control.Applicative
+import Control.Exception as C
 import Control.Monad.Extra
 import Control.Monad.IO.Class
 import Data.Maybe
 import Data.Binary
 import Data.List
+import Data.Tuple.Extra
+import qualified Data.HashSet as Set
 import qualified System.Directory as IO
 import qualified System.Environment.Extra as IO
 
@@ -97,9 +101,9 @@
     get = do
         i <- getWord8
         case i of
-            0 -> liftM  GetDir get
-            1 -> liftM2 GetDirFiles get get
-            2 -> liftM  GetDirDirs get
+            0 -> GetDir <$> get
+            1 -> GetDirFiles <$> get <*> get
+            2 -> GetDirDirs <$> get
 
     put (GetDir x) = putWord8 0 >> put x
     put (GetDirFiles x y) = putWord8 1 >> put x >> put y
@@ -107,29 +111,29 @@
 
 
 instance Rule DoesFileExistQ DoesFileExistA where
-    storedValue _ (DoesFileExistQ x) = fmap (Just . DoesFileExistA) $ IO.doesFileExist x
+    storedValue _ (DoesFileExistQ x) = (Just . DoesFileExistA) <$> IO.doesFileExist x
 
 instance Rule DoesDirectoryExistQ DoesDirectoryExistA where
-    storedValue _ (DoesDirectoryExistQ x) = fmap (Just . DoesDirectoryExistA) $ IO.doesDirectoryExist x
+    storedValue _ (DoesDirectoryExistQ x) = (Just . DoesDirectoryExistA) <$> IO.doesDirectoryExist x
 
 instance Rule GetEnvQ GetEnvA where
-    storedValue _ (GetEnvQ x) = fmap (Just . GetEnvA) $ IO.lookupEnv x
+    storedValue _ (GetEnvQ x) = (Just . GetEnvA) <$> IO.lookupEnv x
 
 instance Rule GetDirectoryQ GetDirectoryA where
-    storedValue _ x = fmap Just $ getDir x
+    storedValue _ x = Just <$> getDir x
 
 
 -- | This function is not actually exported, but Haddock is buggy. Please ignore.
 defaultRuleDirectory :: Rules ()
 defaultRuleDirectory = do
     rule $ \(DoesFileExistQ x) -> Just $
-        liftIO $ fmap DoesFileExistA $ IO.doesFileExist x
+        liftIO $ DoesFileExistA <$> IO.doesFileExist x
     rule $ \(DoesDirectoryExistQ x) -> Just $
-        liftIO $ fmap DoesDirectoryExistA $ IO.doesDirectoryExist x
+        liftIO $ DoesDirectoryExistA <$> IO.doesDirectoryExist x
     rule $ \(x :: GetDirectoryQ) -> Just $
         liftIO $ getDir x
     rule $ \(GetEnvQ x) -> Just $
-        liftIO $ fmap GetEnvA $ IO.lookupEnv x
+        liftIO $ GetEnvA <$> IO.lookupEnv x
 
 
 -- | Returns 'True' if the file exists. The existence of the file is tracked as a
@@ -138,7 +142,7 @@
 --   You should not call 'doesFileExist' on files which can be created by the build system.
 doesFileExist :: FilePath -> Action Bool
 doesFileExist file = do
-    DoesFileExistA res <- apply1 $ DoesFileExistQ file
+    DoesFileExistA res <- apply1 $ DoesFileExistQ $ toStandard file
     return res
 
 -- | Returns 'True' if the directory exists. The existence of the directory is tracked as a
@@ -147,19 +151,30 @@
 --   You should not call 'doesDirectoryExist' on directories which can be created by the build system.
 doesDirectoryExist :: FilePath -> Action Bool
 doesDirectoryExist file = do
-    DoesDirectoryExistA res <- apply1 $ DoesDirectoryExistQ file
+    DoesDirectoryExistA res <- apply1 $ DoesDirectoryExistQ $ toStandard file
     return res
 
 -- | Return 'Just' the value of the environment variable, or 'Nothing'
 --   if the variable is not set. The environment variable is tracked as a
 --   dependency, and if it changes the rule will rerun in subsequent builds.
+--   This function is a tracked version of 'getEnv'/'lookupEnv' from the base library.
+--
+-- @
+-- flags <- getEnv \"CFLAGS\"
+-- 'cmd' \"gcc -c\" [out] (maybe [] words flags)
+-- @
 getEnv :: String -> Action (Maybe String)
 getEnv var = do
     GetEnvA res <- apply1 $ GetEnvQ var
     return res
 
--- | Return the value of the environment variable, or the default value if it
---   not set. Similar to 'getEnv'.
+-- | Return the value of the environment variable (second argument), or the
+--   default value (first argument) if it is not set. Similar to 'getEnv'.
+--
+-- @
+-- flags <- getEnvWithDefault \"-Wall\" \"CFLAGS\"
+-- 'cmd' \"gcc -c\" [out] flags
+-- @
 getEnvWithDefault :: String -> String -> Action String
 getEnvWithDefault def var = fromMaybe def <$> getEnv var
 
@@ -174,7 +189,7 @@
 
 -- | Get the files anywhere under a directory that match any of a set of patterns.
 --   For the interpretation of the patterns see '?=='. All results will be
---   relative to the 'FilePath' argument. The result is tracked as a
+--   relative to the directory argument. The result is tracked as a
 --   dependency, and if it changes the rule will rerun in subsequent builds.
 --   Some examples:
 --
@@ -185,11 +200,28 @@
 -- >     -- All .hs or .lhs in the Modules directory
 -- >     -- If Modules/foo.hs and Modules/foo.lhs exist, it will return ["foo.hs","foo.lhs"]
 --
---   If you require a qualified file name it is often easier to use @\"\"@ as 'FilePath' argument,
+--   If you require a qualified file name it is often easier to use @\"\"@ as the 'FilePath' argument,
 --   for example the following two expressions are equivalent:
 --
 -- > fmap (map ("Config" </>)) (getDirectoryFiles "Config" ["//*.xml"])
 -- > getDirectoryFiles "" ["Config//*.xml"]
+--
+--   If the first argument directory does not exist it will raise an error.
+--   If @foo@ does not exist, then the first of these error, but the second will not.
+--
+-- > getDirectoryFiles "foo" ["//*"] -- error
+-- > getDirectoryFiles "" ["foo//*"] -- returns []
+--
+--   This function is tracked and serves as a dependency. If a rule calls
+--   @getDirectoryFiles \"\" [\"*.c\"]@ and someone adds @foo.c@ to the
+--   directory, that rule will rebuild. If someone changes one of the @.c@ files,
+--   but the /list/ of @.c@ files doesn't change, then it will not rebuild.
+--   As a consequence of being tracked, if the contents change during the build
+--   (e.g. you are generating @.c@ files in this directory) then the build not reach
+--   a stable point, which is an error - detected by running with @--lint@.
+--   You should only call this function returning source files.
+--
+--   For an untracked variant see 'getDirectoryFilesIO'.
 getDirectoryFiles :: FilePath -> [FilePattern] -> Action [FilePath]
 getDirectoryFiles x f = getDirAction $ GetDirFiles x f
 
@@ -197,7 +229,6 @@
 --   All directories are relative to the argument directory. The result is tracked as a
 --   dependency, and if it changes the rule will rerun in subsequent builds.
 --
---
 -- > getDirectoryDirs "/Users"
 -- >    -- Return all directories in the /Users directory
 -- >    -- e.g. ["Emily","Henry","Neil"]
@@ -216,61 +247,68 @@
 answer = GetDirectoryA . sort
 
 getDir :: GetDirectoryQ -> IO GetDirectoryA
-getDir GetDir{..} = fmap answer $ contents dir
+getDir GetDir{..} = answer <$> contents dir
 
 getDir GetDirDirs{..} = fmap answer $ filterM f =<< contents dir
     where f x = IO.doesDirectoryExist $ dir </> x
 
-getDir GetDirFiles{..} = fmap answer $ concatMapM f $ directories pat
-    where
-        test = let ps = map (?==) pat in \x -> any ($ x) ps
+getDir GetDirFiles{..} = answer <$> getDirectoryFilesIO dir pat
 
-        f (dir2,False) = do
-            xs <- fmap (map (dir2 </>)) $ contents $ dir </> dir2
-            flip filterM xs $ \x -> if not $ test x then return False else fmap not $ IO.doesDirectoryExist $ dir </> x
 
-        f (dir2,True) = do
-            xs <- fmap (map (dir2 </>)) $ contents $ dir </> dir2
-            (dirs,files) <- partitionM (\x -> IO.doesDirectoryExist $ dir </> x) xs
-            rest <- concatMapM (\d -> f (d, True)) dirs
-            return $ filter test files ++ rest
+-- | A version of 'getDirectoryFiles' that is in IO, and thus untracked.
+getDirectoryFilesIO :: FilePath -> [FilePattern] -> IO [FilePath]
+-- Known infelicity: on Windows, if you search for "foo", but have the file "FOO",
+-- it will match if on its own, or not if it is paired with "*", since that forces
+-- a full directory scan, and then it uses Haskell equality (case sensitive)
+getDirectoryFilesIO root pat = f "" $ snd $ walk pat
+    where
+        -- Even after we know they are there because we called contents, we still have to check they are directories/files
+        -- as required
+        f dir (Walk op) = f dir . WalkTo . op =<< contents (root </> dir)
+        f dir (WalkTo (files, dirs)) = do
+            files <- filterM (IO.doesFileExist . (root </>)) $ map (dir </>) files
+            dirs <- concatMapM (uncurry f) =<< filterM (IO.doesDirectoryExist . (root </>) . fst) (map (first (dir </>)) dirs)
+            return $ files ++ dirs
 
 
 -- | Remove all files and directories that match any of the patterns within a directory.
 --   Some examples:
 --
 -- @
--- 'removeFiles' \"output\" [\"\/\/*\"]
--- 'removeFiles' \".\" [\"\/\/*.hi\",\"\/\/*.o\"]
+-- 'removeFiles' \"output\" [\"\/\/*\"]        -- delete everything inside \'output\'
+-- 'removeFiles' \"output\" [\"\/\/\"]         -- delete \'output\' itself
+-- 'removeFiles' \".\" [\"\/\/*.hi\",\"\/\/*.o\"] -- delete all \'.hi\' and \'.o\' files
 -- @
 --
---   Any directories that become empty after deleting items from within them will themselves be deleted,
---   up to (and including) the containing directory.
+--   If the argument directory is missing no error is raised.
+--   This function will follow symlinks, so should be used with care.
+--
 --   This function is often useful when writing a @clean@ action for your build system,
 --   often as a 'phony' rule.
 removeFiles :: FilePath -> [FilePattern] -> IO ()
-removeFiles dir pat = do
-    b <- IO.doesDirectoryExist dir
-    when b $ void $ f ""
+removeFiles dir pat =
+    whenM (IO.doesDirectoryExist dir) $ do
+        let (b,w) = walk pat
+        if b then removeDir dir else f dir w
     where
-        -- because it is generate and match anything like ../ will be ignored, since we never generate ..
-        -- therefore we can safely know we never escape the containing directory
-        test = let ps = map (?==) pat in \x -> any ($ x) ps
+        f dir (Walk op) = f dir . WalkTo . op =<< contents dir
+        f dir (WalkTo (files, dirs)) = do
+            forM_ files $ \fil ->
+                try $ removeItem $ dir </> fil :: IO (Either IOException ())
+            let done = Set.fromList files
+            forM_ (filter (not . flip Set.member done . fst) dirs) $ \(d,w) -> do
+                let dir2 = dir </> d
+                whenM (IO.doesDirectoryExist dir2) $ f dir2 w
 
-        -- dir </> dir2 is the part to operate on, return True if you deleted the directory
-        f :: FilePath -> IO Bool
-        f dir2 | test dir2 = do
-            IO.removeDirectoryRecursive $ dir </> dir2
-            return True
-        f dir2 = do
-            xs <- fmap (map (dir2 </>)) $ contents $ dir </> dir2
-            (dirs,files) <- partitionM (\x -> IO.doesDirectoryExist $ dir </> x) xs
-            noDirs <- fmap and $ mapM f dirs
-            let (del,keep) = partition test files
-            forM del $ \d -> IO.removeFile $ dir </> d
-            let die = noDirs && null keep && not (null xs)
-            when die $ IO.removeDirectory $ dir </> dir2
-            return die
+        removeItem :: FilePath -> IO ()
+        removeItem x = IO.removeFile x `C.catch` \(_ :: IOException) -> removeDir x
+
+        -- In newer GHC's removeDirectoryRecursive is probably better, but doesn't follow
+        -- symlinks, so it's got different behaviour
+        removeDir :: FilePath -> IO ()
+        removeDir x = do
+            mapM_ (removeItem . (x </>)) =<< contents x
+            IO.removeDirectory x
 
 
 -- | Remove files, like 'removeFiles', but executed after the build completes successfully.
diff --git a/src/Development/Shake/Rules/File.hs b/src/Development/Shake/Rules/File.hs
--- a/src/Development/Shake/Rules/File.hs
+++ b/src/Development/Shake/Rules/File.hs
@@ -2,7 +2,7 @@
 {-# LANGUAGE ViewPatterns #-}
 
 module Development.Shake.Rules.File(
-    need, needBS, needed, neededBS, want,
+    need, needBS, needed, neededBS, needNorm, want,
     trackRead, trackWrite, trackAllow,
     defaultRuleFile,
     (%>), (|%>), (?>), phony, (~>), phonys,
@@ -73,11 +73,13 @@
         ChangeModtime -> bool $ x1 == y1
         ChangeDigest -> bool $ x2 == y2 && x3 == y3
         ChangeModtimeOrDigest -> bool $ x1 == y1 && x2 == y2 && x3 == y3
-        _ -> if x1 == y1 then EqualCheap
-             else if x2 == y2 && x3 == y3 then EqualExpensive
-             else NotEqual
+        _ | x1 == y1 -> EqualCheap
+          | x2 == y2 && x3 == y3 -> EqualExpensive
+          | otherwise -> NotEqual
         where bool b = if b then EqualCheap else NotEqual
 
+
+-- | Arguments: options; is the file an input; a message for failure if the file does not exist; filename
 storedValueError :: ShakeOptions -> Bool -> String -> FileQ -> IO FileA
 {-
 storedValueError opts False msg x | False && not (shakeOutputCheck opts) = do
@@ -111,9 +113,16 @@
 --
 --   Usually @need [foo,bar]@ is preferable to @need [foo] >> need [bar]@ as the former allows greater
 --   parallelism, while the latter requires @foo@ to finish building before starting to build @bar@.
+--
+--   This function should not be called with wildcards (e.g. @*.txt@ - use 'getDirectoryFiles' to expand them),
+--   environment variables (e.g. @$HOME@ - use 'getEnv' to expand them) or directories (directories cannot be
+--   tracked directly - track files within the directory instead).
 need :: [FilePath] -> Action ()
 need xs = (apply $ map (FileQ . packU_ . filepathNormalise . unpackU_ . packU) xs :: Action [FileA]) >> return ()
 
+needNorm :: [FilePath] -> Action ()
+needNorm xs = (apply $ map (FileQ . packU) xs :: Action [FileA]) >> return ()
+
 needBS :: [BS.ByteString] -> Action ()
 needBS xs = (apply $ map (FileQ . packU_ . filepathNormalise) xs :: Action [FileA]) >> return ()
 
@@ -150,13 +159,13 @@
 
 -- | Track that a file was read by the action preceeding it. If 'shakeLint' is activated
 --   then these files must be dependencies of this rule. Calls to 'trackRead' are
---   automatically inserted in 'LintTracker' mode.
+--   automatically inserted in 'LintFSATrace' mode.
 trackRead :: [FilePath] -> Action ()
 trackRead = mapM_ (trackUse . FileQ . packU)
 
 -- | Track that a file was written by the action preceeding it. If 'shakeLint' is activated
 --   then these files must either be the target of this rule, or never referred to by the build system.
---   Calls to 'trackWrite' are automatically inserted in 'LintTracker' mode.
+--   Calls to 'trackWrite' are automatically inserted in 'LintFSATrace' mode.
 trackWrite :: [FilePath] -> Action ()
 trackWrite = mapM_ (trackChange . FileQ . packU)
 
@@ -196,15 +205,26 @@
         liftIO $ storedValueError opts False ("Error, rule " ++ help ++ " failed to build file:") $ FileQ x_
 
 
--- | Declare a phony action -- an action that does not produce a file, and will be rerun
---   in every execution that requires it. You can demand 'phony' rules using 'want' \/ 'need'.
---   Phony actions are never executed more than once in a single build run.
+-- | Declare a Make-style phony action.  A phony target does not name
+--   a file (despite living in the same namespace as file rules);
+--   rather, it names some action to be executed when explicitly
+--   requested.  You can demand 'phony' rules using 'want'. (And 'need',
+--   although that's not recommended.)
 --
---   Phony actions are intended to define command-line abbreviations. If you 'need' a phony action
---   in a rule then every execution where that rule is required will rerun both the rule and the phony
---   action.
+--   Phony actions are intended to define recipes that can be executed
+--   by the user. If you 'need' a phony action in a rule then every
+--   execution where that rule is required will rerun both the rule and
+--   the phony action.  However, note that phony actions are never
+--   executed more than once in a single build run.
+--
+--   In make, the @.PHONY@ attribute on non-file-producing rules has a
+--   similar effect.  However, while in make it is acceptable to omit
+--   the @.PHONY@ attribute as long as you don't create the file in
+--   question, a Shake rule which behaves this way will fail lint.
+--   Use a phony rule!  For file-producing rules which should be
+--   rerun every execution of Shake, see 'Development.Shake.alwaysRerun'.
 phony :: String -> Action () -> Rules ()
-phony name act = phonys $ \s -> if s == name then Just act else Nothing
+phony (toStandard -> name) act = phonys $ \s -> if s == name then Just act else Nothing
 
 -- | A predicate version of 'phony', return 'Just' with the 'Action' for the matching rules.
 phonys :: (String -> Maybe (Action ())) -> Rules ()
@@ -244,8 +264,8 @@
     let (simp,other) = partition simple pats
     case simp of
         [] -> return ()
-        [p] -> root "with |%>" (\x -> toStandard x == p) act
-        ps -> let ps = Set.fromList pats in root "with |%>" (flip Set.member ps . toStandard) act
+        [p] -> let pp = toStandard p in root "with |%>" (\x -> toStandard x == pp) act
+        ps -> let ps = Set.fromList $ map toStandard pats in root "with |%>" (flip Set.member ps . toStandard) act
     unless (null other) $
         let ps = map (?==) other in priority 0.5 $ root "with |%>" (\x -> any ($ x) ps) act
 
diff --git a/src/Development/Shake/Rules/Files.hs b/src/Development/Shake/Rules/Files.hs
--- a/src/Development/Shake/Rules/Files.hs
+++ b/src/Development/Shake/Rules/Files.hs
@@ -9,6 +9,8 @@
 import Data.Maybe
 import Data.List.Extra
 import System.Directory
+import Control.Applicative
+import Prelude
 
 import Development.Shake.Core hiding (trackAllow)
 import General.Extra
@@ -38,7 +40,7 @@
 
 
 instance Rule FilesQ FilesA where
-    storedValue opts (FilesQ xs) = fmap (fmap FilesA . sequence) $ mapM (storedValue opts) xs
+    storedValue opts (FilesQ xs) = (fmap FilesA . sequence) <$> mapM (storedValue opts) xs
     equalValue opts (FilesQ qs) (FilesA xs) (FilesA ys)
         | let n = length qs in n /= length xs || n /= length ys = NotEqual
         | otherwise = foldr and_ EqualCheap (zipWith3 (equalValue opts) qs xs ys)
@@ -62,11 +64,10 @@
 --   on the @.o@. When defining rules that build multiple files, all the 'FilePattern' values must
 --   have the same sequence of @\/\/@ and @*@ wildcards in the same order.
 --   This function will create directories for the result files, if necessary.
---   Think of it as the OR (@||@) equivalent of '%>'.
 (&%>) :: [FilePattern] -> ([FilePath] -> Action ()) -> Rules ()
 ps &%> act
     | not $ compatible ps = error $ unlines $
-        ["All patterns to &%> must have the same number and position of // and * wildcards"] ++
+        "All patterns to &%> must have the same number and position of // and * wildcards" :
         ["* " ++ p ++ (if compatible [p, head ps] then "" else " (incompatible)") | p <- ps]
     | otherwise = do
         forM_ ps $ \p ->
@@ -94,9 +95,9 @@
 --   As an example of a function satisfying the invariaint:
 --
 -- @
---test x | 'Development.Shake.FilePath.takeExtension' x \`elem\` [\".hi\",\".o\"]
+-- test x | 'Development.Shake.FilePath.takeExtension' x \`elem\` [\".hi\",\".o\"]
 --        = Just ['Development.Shake.FilePath.dropExtension' x 'Development.Shake.FilePath.<.>' \"hi\", 'Development.Shake.FilePath.dropExtension' x 'Development.Shake.FilePath.<.>' \"o\"]
---test _ = Nothing
+-- test _ = Nothing
 -- @
 --
 --   Regardless of whether @Foo.hi@ or @Foo.o@ is passed, the function always returns @[Foo.hi, Foo.o]@.
@@ -110,7 +111,7 @@
     let checkedTest x = case normTest x of
             Nothing -> Nothing
             Just ys | x `notElem` ys -> error $ unlines $
-                ["Invariant broken in &?>, did not return the input (after normalisation)."] ++
+                "Invariant broken in &?>, did not return the input (after normalisation)." :
                 inputOutput "" x ys
             Just ys | bad:_ <- filter ((/= Just ys) . normTest) ys -> error $ unlines $
                 ["Invariant broken in &?>, not equal for all arguments (after normalisation)."] ++
@@ -137,7 +138,8 @@
 getFileTimes :: String -> [FileQ] -> Action FilesA
 getFileTimes name xs = do
     opts <- getShakeOptions
-    ys <- liftIO $ mapM (storedValue opts) xs
+    let opts2 = if shakeChange opts == ChangeModtimeAndDigestInput then opts{shakeChange=ChangeModtime} else opts
+    ys <- liftIO $ mapM (storedValue opts2) xs
     case sequence ys of
         Just ys -> return $ FilesA ys
         Nothing | not $ shakeCreationCheck opts -> return $ FilesA []
diff --git a/src/Development/Shake/Rules/Oracle.hs b/src/Development/Shake/Rules/Oracle.hs
--- a/src/Development/Shake/Rules/Oracle.hs
+++ b/src/Development/Shake/Rules/Oracle.hs
@@ -1,34 +1,23 @@
-{-# LANGUAGE MultiParamTypeClasses, GeneralizedNewtypeDeriving, DeriveDataTypeable, ScopedTypeVariables #-}
--- Allows the user to violate the functional dependency, but it has a runtime check so still safe
+{-# LANGUAGE MultiParamTypeClasses, GeneralizedNewtypeDeriving, DeriveDataTypeable, ScopedTypeVariables, ConstraintKinds #-}
 {-# LANGUAGE UndecidableInstances #-}
 
-{-# LANGUAGE CPP #-}
-#if __GLASGOW_HASKELL__ >= 704
-{-# LANGUAGE ConstraintKinds #-}
-#endif
-
 module Development.Shake.Rules.Oracle(
     addOracle, askOracle, askOracleWith
     ) where
 
 import Development.Shake.Core
 import Development.Shake.Classes
+import Control.Applicative
+import Prelude
 
 
--- Use should type names, since the names appear in the Haddock, and are too long if they are in full
+-- Use short type names, since the names appear in the Haddock, and are too long if they are in full
 newtype OracleQ question = OracleQ question
     deriving (Show,Typeable,Eq,Hashable,Binary,NFData)
 newtype OracleA answer = OracleA answer
     deriving (Show,Typeable,Eq,Hashable,Binary,NFData)
 
-instance (
-#if __GLASGOW_HASKELL__ >= 704
-    ShakeValue q, ShakeValue a
-#else
-    Show q, Typeable q, Eq q, Hashable q, Binary q, NFData q,
-    Show a, Typeable a, Eq a, Hashable a, Binary a, NFData a
-#endif
-    ) => Rule (OracleQ q) (OracleA a) where
+instance (ShakeValue q, ShakeValue a) => Rule (OracleQ q) (OracleA a) where
     storedValue _ _ = return Nothing
 
 
@@ -51,9 +40,10 @@
 --
 -- * Each call to 'addOracle' must use a different type of question.
 --
--- * Actions passed to 'addOracle' will be run in every build they are required,
---   but if their value does not change they will not invalidate any rules depending on them.
---   To get a similar behaviour using data stored in files, see 'Development.Shake.alwaysRerun'.
+-- * Actions passed to 'addOracle' will be run in every build they are required, even if nothing else changes,
+--   so be careful of slow actions.
+--   If the result of an oracle does not change it will not invalidate any rules depending on it.
+--   To always rerun files rules see 'Development.Shake.alwaysRerun'.
 --
 -- * If the value returned by 'askOracle' is ignored then 'askOracleWith' may help avoid ambiguous type messages.
 --   Alternatively, use the result of 'addOracle', which is 'askOracle' restricted to the correct type.
@@ -61,58 +51,37 @@
 --   As a more complex example, consider tracking Haskell package versions:
 --
 -- @
---newtype GhcPkgList = GhcPkgList () deriving (Show,Typeable,Eq,Hashable,Binary,NFData)
---newtype GhcPkgVersion = GhcPkgVersion String deriving (Show,Typeable,Eq,Hashable,Binary,NFData)
+-- newtype GhcPkgList = GhcPkgList () deriving (Show,Typeable,Eq,Hashable,Binary,NFData)
+-- newtype GhcPkgVersion = GhcPkgVersion String deriving (Show,Typeable,Eq,Hashable,Binary,NFData)
 --
---rules = do
---    getPkgList \<- 'addOracle' $ \\GhcPkgList{} -> do
---        Stdout out <- 'Development.Shake.cmd' \"ghc-pkg list --simple-output\"
---        return [(reverse b, reverse a) | x <- words out, let (a,_:b) = break (== \'-\') $ reverse x]
---    --
---    getPkgVersion \<- 'addOracle' $ \\(GhcPkgVersion pkg) -> do
---        pkgs <- getPkgList $ GhcPkgList ()
---        return $ lookup pkg pkgs
---    --
---    \"myrule\" %> \\_ -> do
---        getPkgVersion $ GhcPkgVersion \"shake\"
---        ... rule using the shake version ...
+-- rules = do
+--     getPkgList \<- 'addOracle' $ \\GhcPkgList{} -> do
+--         Stdout out <- 'Development.Shake.cmd' \"ghc-pkg list --simple-output\"
+--         return [(reverse b, reverse a) | x <- words out, let (a,_:b) = break (== \'-\') $ reverse x]
+--
+--     getPkgVersion \<- 'addOracle' $ \\(GhcPkgVersion pkg) -> do
+--         pkgs <- getPkgList $ GhcPkgList ()
+--         return $ lookup pkg pkgs
+--
+--     \"myrule\" %> \\_ -> do
+--         getPkgVersion $ GhcPkgVersion \"shake\"
+--         ... rule using the shake version ...
 -- @
 --
 --   Using these definitions, any rule depending on the version of @shake@
 --   should call @getPkgVersion $ GhcPkgVersion \"shake\"@ to rebuild when @shake@ is upgraded.
-addOracle :: (
-#if __GLASGOW_HASKELL__ >= 704
-    ShakeValue q, ShakeValue a
-#else
-    Show q, Typeable q, Eq q, Hashable q, Binary q, NFData q,
-    Show a, Typeable a, Eq a, Hashable a, Binary a, NFData a
-#endif
-    ) => (q -> Action a) -> Rules (q -> Action a)
+addOracle :: (ShakeValue q, ShakeValue a) => (q -> Action a) -> Rules (q -> Action a)
 addOracle act = do
-    rule $ \(OracleQ q) -> Just $ fmap OracleA $ act q
+    rule $ \(OracleQ q) -> Just $ OracleA <$> act q
     return askOracle
 
 
 -- | Get information previously added with 'addOracle'. The question/answer types must match those provided
 --   to 'addOracle'.
-askOracle :: (
-#if __GLASGOW_HASKELL__ >= 704
-    ShakeValue q, ShakeValue a
-#else
-    Show q, Typeable q, Eq q, Hashable q, Binary q, NFData q,
-    Show a, Typeable a, Eq a, Hashable a, Binary a, NFData a
-#endif
-    ) => q -> Action a
+askOracle :: (ShakeValue q, ShakeValue a) => q -> Action a
 askOracle question = do OracleA answer <- apply1 $ OracleQ question; return answer
 
 -- | Get information previously added with 'addOracle'. The second argument is not used, but can
 --   be useful to fix the answer type, avoiding ambiguous type error messages.
-askOracleWith :: (
-#if __GLASGOW_HASKELL__ >= 704
-    ShakeValue q, ShakeValue a
-#else
-    Show q, Typeable q, Eq q, Hashable q, Binary q, NFData q,
-    Show a, Typeable a, Eq a, Hashable a, Binary a, NFData a
-#endif
-    ) => q -> a -> Action a
+askOracleWith :: (ShakeValue q, ShakeValue a) => q -> a -> Action a
 askOracleWith question _ = askOracle question
diff --git a/src/Development/Shake/Rules/OrderOnly.hs b/src/Development/Shake/Rules/OrderOnly.hs
--- a/src/Development/Shake/Rules/OrderOnly.hs
+++ b/src/Development/Shake/Rules/OrderOnly.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE MultiParamTypeClasses, GeneralizedNewtypeDeriving, DeriveDataTypeable #-}
 
 module Development.Shake.Rules.OrderOnly(
      orderOnly, orderOnlyBS
@@ -22,11 +21,10 @@
 -- @
 --
 --   If @header.h@ is included by @source.c@ then the call to 'needMakefileDependencies' will cause
---   it to be added as a real dependency. If it isn't, then the rule won't rebuild if it changes,
---   and you will have lost some opportunity for parallelism.
+--   it to be added as a real dependency. If it isn't, then the rule won't rebuild if it changes.
 orderOnly :: [FilePath] -> Action ()
-orderOnly = unsafeIgnoreDependencies . need
+orderOnly = orderOnlyAction . need
 
 
 orderOnlyBS :: [BS.ByteString] -> Action ()
-orderOnlyBS = unsafeIgnoreDependencies . needBS
+orderOnlyBS = orderOnlyAction . needBS
diff --git a/src/Development/Shake/Rules/Rerun.hs b/src/Development/Shake/Rules/Rerun.hs
--- a/src/Development/Shake/Rules/Rerun.hs
+++ b/src/Development/Shake/Rules/Rerun.hs
@@ -30,6 +30,11 @@
 --     'Development.Shake.Stdout' stdout <- 'Development.Shake.cmd' \"ghc --numeric-version\"
 --     'Development.Shake.writeFileChanged' out stdout
 -- @
+--
+--   In make, the @.PHONY@ attribute on file-producing rules has a similar effect.
+--
+--   Note that 'alwaysRerun' is applied when a rule is executed. Modifying an existing rule
+--   to insert 'alwaysRerun' will /not/ cause that rule to rerun next time.
 alwaysRerun :: Action ()
 alwaysRerun = do AlwaysRerunA _ <- apply1 $ AlwaysRerunQ (); return ()
 
diff --git a/src/Development/Shake/Storage.hs b/src/Development/Shake/Storage.hs
--- a/src/Development/Shake/Storage.hs
+++ b/src/Development/Shake/Storage.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE ScopedTypeVariables, PatternGuards, NamedFieldPuns, FlexibleInstances, MultiParamTypeClasses #-}
+{-# LANGUAGE ScopedTypeVariables, PatternGuards, RecordWildCards, FlexibleInstances, MultiParamTypeClasses #-}
 {-
 This module stores the meta-data so its very important its always accurate
 We can't rely on getting any exceptions or termination at the end, so we'd better write out a journal
@@ -12,6 +12,7 @@
 import General.Binary
 import Development.Shake.Types
 import General.Timing
+import General.FileLock
 
 import Data.Tuple.Extra
 import Control.Exception.Extra
@@ -61,7 +62,10 @@
     -> w                        -- ^ Witness
     -> (Map k v -> (k -> v -> IO ()) -> IO a)  -- ^ Execute
     -> IO a
-withStorage ShakeOptions{shakeVerbosity,shakeOutput,shakeVersion,shakeVersionIgnore,shakeFlush,shakeFiles,shakeStorageLog} diagnostic witness act = do
+withStorage ShakeOptions{..} diagnostic witness act = do
+  diagnostic $ "Before fileLock on " ++ shakeFiles </> ".shake.lock"
+  withLockFile (shakeFiles </> ".shake.lock") $ do
+    diagnostic "After fileLock"
     let dbfile = shakeFiles </> ".shake.database"
         bupfile = shakeFiles </> ".shake.backup"
     createDirectoryIfMissing True shakeFiles
@@ -70,7 +74,7 @@
     b <- doesFileExist bupfile
     when b $ do
         unexpected "Backup file exists, restoring over the previous file\n"
-        diagnostic $ "Backup file move to original"
+        diagnostic "Backup file move to original"
         ignore $ removeFile dbfile
         renameFile bupfile dbfile
 
@@ -94,7 +98,7 @@
             continue h Map.empty
          else
             -- make sure you are not handling exceptions from inside
-            join $ handleJust (\e -> if asyncException e then Nothing else Just e) (\err -> do
+            join $ handleBool (not . asyncException) (\err -> do
                 msg <- showException err
                 outputErr $ unlines $
                     ("Error when reading Shake database " ++ dbfile) :
@@ -147,7 +151,7 @@
                                 hSetFileSize h $ now - slop
                                 hSeek h AbsoluteSeek $ now - slop
                                 hFlush h
-                                diagnostic $ "Drop complete"
+                                diagnostic "Drop complete"
                             return $ continue h mp
                          else do
                             addTiming "Database compression"
diff --git a/src/Development/Shake/Types.hs b/src/Development/Shake/Types.hs
--- a/src/Development/Shake/Types.hs
+++ b/src/Development/Shake/Types.hs
@@ -8,8 +8,13 @@
 
 import Data.Data
 import Data.List
+import Data.Dynamic
+import qualified Data.HashMap.Strict as HashMap
 import Development.Shake.Progress
+import Development.Shake.FilePattern
 import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.UTF8 as UTF8
+import Development.Shake.CmdOption
 
 
 -- | The current assumptions made by the build system, used by 'shakeAssume'. These options
@@ -33,7 +38,7 @@
         -- ^ /This assumption is unsafe, and may lead to incorrect build results in this run/.
         --   Assume that all rules reached are clean in this run. Only useful for benchmarking, to remove any overhead
         --   from running 'Development.Shake.Rule.storedValue' operations.
-    deriving (Eq,Ord,Show,Data,Typeable,Bounded,Enum)
+      deriving (Eq,Ord,Show,Read,Typeable,Data,Enum,Bounded)
 
 
 -- | Which lint checks to perform, used by 'shakeLint'.
@@ -41,11 +46,10 @@
     = LintBasic
         -- ^ The most basic form of linting. Checks that the current directory does not change and that results do not change after they
         --   are first written. Any calls to 'needed' will assert that they do not cause a rule to be rebuilt.
-    | LintTracker
-        -- ^ Track which files are accessed by command line programs run by 'command' or 'cmd', using @tracker.exe@ as supplied
-        --   with the Microsoft .NET 4.5 SDK (Windows only). Also performs all checks from 'LintBasic'. Note that some programs are not
-        --   tracked properly, particularly cygwin programs (it seems).
-    deriving (Eq,Ord,Show,Data,Typeable,Bounded,Enum)
+    | LintFSATrace
+        -- ^ Track which files are accessed by command line programs
+        -- using <https://github.com/jacereda/fsatrace fsatrace>.
+      deriving (Eq,Ord,Show,Read,Typeable,Data,Enum,Bounded)
 
 
 -- | How should you determine if a file has changed, used by 'shakeChange'. The most common values are
@@ -66,7 +70,7 @@
     | ChangeModtimeOrDigest
         -- ^ A file is rebuilt if either its modification time or its digest has changed. A @touch@ will force a rebuild,
         --   but even if a files modification time is reset afterwards, changes will also cause a rebuild.
-    deriving (Eq,Ord,Show,Data,Typeable,Bounded,Enum)
+      deriving (Eq,Ord,Show,Read,Typeable,Data,Enum,Bounded)
 
 
 -- | Options to control the execution of Shake, usually specified by overriding fields in
@@ -74,8 +78,8 @@
 --
 --   @ 'shakeOptions'{'shakeThreads'=4, 'shakeReport'=[\"report.html\"]} @
 --
---   The 'Data' instance for this type reports the 'shakeProgress' and 'shakeOutput' fields as having the abstract type 'Function',
---   because 'Data' cannot be defined for functions.
+--   The 'Data' instance for this type reports the 'shakeProgress' and 'shakeOutput' fields as having the abstract type 'Hidden',
+--   because 'Data' cannot be defined for functions or 'TypeRep's.
 data ShakeOptions = ShakeOptions
     {shakeFiles :: FilePath
         -- ^ Defaults to @.shake@. The directory used for storing Shake metadata files.
@@ -104,6 +108,12 @@
         --   otherwise it will write HTML.
     ,shakeLint :: Maybe Lint
         -- ^ Defaults to 'Nothing'. Perform sanity checks during building, see 'Lint' for details.
+    ,shakeLintInside :: [FilePath]
+        -- ^ Directories in which the files will be tracked by the linter.
+    ,shakeLintIgnore :: [FilePattern]
+        -- ^ File patterns which are ignored from linter tracking, a bit like calling 'trackAllow' in every rule.
+    ,shakeCommandOptions :: [CmdOption]
+        -- ^ Defaults to @[]@. Additional options to be passed to all command invocations.
     ,shakeFlush :: Maybe Double
         -- ^ Defaults to @'Just' 10@. How often to flush Shake metadata files in seconds, or 'Nothing' to never flush explicitly.
         --   It is possible that on abnormal termination (not Haskell exceptions) any rules that completed in the last
@@ -133,7 +143,7 @@
 --    ,shakeOutputCheck :: Bool
 --        -- ^ Default to 'True'. If a file produced by a rule changes, should you rebuild it.
     ,shakeLiveFiles :: [FilePath]
-        -- ^ Default to '[]'. After the build system completes, write a list of all files which were /live/ in that run,
+        -- ^ Default to @[]@. After the build system completes, write a list of all files which were /live/ in that run,
         --   i.e. those which Shake checked were valid or rebuilt. Produces best answers if nothing rebuilds.
     ,shakeVersionIgnore :: Bool
         -- ^ Defaults to 'False'. Ignore any differences in 'shakeVersion'.
@@ -146,33 +156,40 @@
         -- ^ Defaults to writing using 'putStrLn'. A function called to output messages from Shake, along with the 'Verbosity' at
         --   which that message should be printed. This function will be called atomically from all other 'shakeOutput' functions.
         --   The 'Verbosity' will always be greater than or higher than 'shakeVerbosity'.
+    ,shakeExtra :: HashMap.HashMap TypeRep Dynamic
+        -- ^ This a map which can be used to store arbitrary extra
+        --   information that a user may need when writing 'Rule's.  The
+        --   correct way to use this is to define a (hidden) newtype to
+        --   use as a key, so that conflicts cannot occur.
     }
     deriving Typeable
 
 -- | The default set of 'ShakeOptions'.
 shakeOptions :: ShakeOptions
 shakeOptions = ShakeOptions
-    ".shake" 1 "1" Normal False [] Nothing (Just 10) Nothing [] False True False
+    ".shake" 1 "1" Normal False [] Nothing [] [] [] (Just 10) Nothing [] False True False
     True ChangeModtime True [] False
     (const $ return ())
-    (const $ BS.putStrLn . BS.pack) -- try and output atomically using BS
+    (const $ BS.putStrLn . UTF8.fromString) -- try and output atomically using BS
+    HashMap.empty
 
 fieldsShakeOptions =
     ["shakeFiles", "shakeThreads", "shakeVersion", "shakeVerbosity", "shakeStaunch", "shakeReport"
-    ,"shakeLint", "shakeFlush", "shakeAssume", "shakeAbbreviations", "shakeStorageLog"
+    ,"shakeLint", "shakeLintInside", "shakeLintIgnore", "shakeCommandOptions"
+    ,"shakeFlush", "shakeAssume", "shakeAbbreviations", "shakeStorageLog"
     ,"shakeLineBuffering", "shakeTimings", "shakeRunCommands", "shakeChange", "shakeCreationCheck"
-    ,"shakeLiveFiles","shakeVersionIgnore","shakeProgress", "shakeOutput"]
+    ,"shakeLiveFiles","shakeVersionIgnore","shakeProgress", "shakeOutput", "shakeExtra"]
 tyShakeOptions = mkDataType "Development.Shake.Types.ShakeOptions" [conShakeOptions]
 conShakeOptions = mkConstr tyShakeOptions "ShakeOptions" fieldsShakeOptions Prefix
-unhide x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 y1 y2 =
-    ShakeOptions x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 (fromFunction y1) (fromFunction y2)
+unhide x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 x19 x20 x21 y1 y2 y3 =
+    ShakeOptions x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 x19 x20 x21 (fromHidden y1) (fromHidden y2) (fromHidden y3)
 
 instance Data ShakeOptions where
-    gfoldl k z (ShakeOptions x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 y1 y2) =
+    gfoldl k z (ShakeOptions x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 x19 x20 x21 y1 y2 y3) =
         z unhide `k` x1 `k` x2 `k` x3 `k` x4 `k` x5 `k` x6 `k` x7 `k` x8 `k` x9 `k` x10 `k` x11 `k`
-        x12 `k` x13 `k` x14 `k` x15 `k` x16 `k` x17 `k` x18 `k`
-        Function y1 `k` Function y2
-    gunfold k z c = k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ z unhide
+        x12 `k` x13 `k` x14 `k` x15 `k` x16 `k` x17 `k` x18 `k` x19 `k` x20 `k` x21 `k`
+        Hidden y1 `k` Hidden y2 `k` Hidden y3
+    gunfold k z c = k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ k $ z unhide
     toConstr ShakeOptions{} = conShakeOptions
     dataTypeOf _ = tyShakeOptions
 
@@ -191,24 +208,26 @@
                 | Just x <- cast x = show (x :: Maybe Lint)
                 | Just x <- cast x = show (x :: Maybe Double)
                 | Just x <- cast x = show (x :: [(String,String)])
-                | Just x <- cast x = show (x :: Function (IO Progress -> IO ()))
-                | Just x <- cast x = show (x :: Function (Verbosity -> String -> IO ()))
+                | Just x <- cast x = show (x :: Hidden (IO Progress -> IO ()))
+                | Just x <- cast x = show (x :: Hidden (Verbosity -> String -> IO ()))
+                | Just x <- cast x = show (x :: Hidden (HashMap.HashMap TypeRep Dynamic))
+                | Just x <- cast x = show (x :: [CmdOption])
                 | otherwise = error $ "Error while showing ShakeOptions, missing alternative for " ++ show (typeOf x)
 
 
 -- | Internal type, copied from Hide in Uniplate
-newtype Function a = Function {fromFunction :: a}
+newtype Hidden a = Hidden {fromHidden :: a}
     deriving Typeable
 
-instance Show (Function a) where show _ = "<function>"
+instance Show (Hidden a) where show _ = "<hidden>"
 
-instance Typeable a => Data (Function a) where
-    gfoldl k z x = z x
+instance Typeable a => Data (Hidden a) where
+    gfoldl k z = z
     gunfold k z c = error "Development.Shake.Types.ShakeProgress: gunfold not implemented - data type has no constructors"
     toConstr _ = error "Development.Shake.Types.ShakeProgress: toConstr not implemented - data type has no constructors"
-    dataTypeOf _ = tyFunction
+    dataTypeOf _ = tyHidden
 
-tyFunction = mkDataType "Development.Shake.Types.Function" []
+tyHidden = mkDataType "Development.Shake.Types.Hidden" []
 
 
 -- | The verbosity data type, used by 'shakeVerbosity'.
@@ -219,11 +238,11 @@
     | Loud   -- ^ Print errors and full command lines when running a 'Development.Shake.command' or 'Development.Shake.cmd' command.
     | Chatty -- ^ Print errors, full command line and status messages when starting a rule.
     | Diagnostic -- ^ Print messages for virtually everything (mostly for debugging).
-      deriving (Eq,Ord,Bounded,Enum,Show,Read,Typeable,Data)
+      deriving (Eq,Ord,Show,Read,Typeable,Data,Enum,Bounded)
 
 -- | An equality check and a cost.
 data EqualCost
     = EqualCheap -- ^ The equality check was cheap.
     | EqualExpensive -- ^ The equality check was expensive, as the results are not trivially equal.
     | NotEqual -- ^ The values are not equal.
-      deriving (Eq,Ord,Bounded,Enum,Show,Read,Typeable,Data)
+      deriving (Eq,Ord,Show,Read,Typeable,Data,Enum,Bounded)
diff --git a/src/Development/Shake/Util.hs b/src/Development/Shake/Util.hs
--- a/src/Development/Shake/Util.hs
+++ b/src/Development/Shake/Util.hs
@@ -47,12 +47,12 @@
 --   Usually used to populate a record structure. As an example of a build system that can use either @gcc@ or @distcc@ for compiling:
 --
 -- @
---import System.Console.GetOpt
+-- import System.Console.GetOpt
 --
---data Flags = Flags {distCC :: Bool} deriving Eq
---flags = [Option \"\" [\"distcc\"] (NoArg $ Right $ \\x -> x{distCC=True}) \"Run distributed.\"]
+-- data Flags = Flags {distCC :: Bool} deriving Eq
+-- flags = [Option \"\" [\"distcc\"] (NoArg $ Right $ \\x -> x{distCC=True}) \"Run distributed.\"]
 --
---main = 'shakeArgsAccumulate' 'shakeOptions' flags (Flags False) $ \\flags targets -> return $ Just $ do
+-- main = 'shakeArgsAccumulate' 'shakeOptions' flags (Flags False) $ \\flags targets -> return $ Just $ do
 --     if null targets then 'want' [\"result.exe\"] else 'want' targets
 --     let compiler = if distCC flags then \"distcc\" else \"gcc\"
 --     \"*.o\" '%>' \\out -> do
@@ -84,7 +84,7 @@
             return Nothing
         else
             act (map fromJust opts) args
-    whenM (readIORef pruning) $ do
+    whenM (readIORef pruning) $
         IO.withTempFile $ \file -> do
             shakeArgsWith opts{shakeLiveFiles=file : shakeLiveFiles opts} flags2 $ \opts args ->
                 act (catMaybes opts) args
diff --git a/src/Development/Shake/Value.hs b/src/Development/Shake/Value.hs
--- a/src/Development/Shake/Value.hs
+++ b/src/Development/Shake/Value.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE ExistentialQuantification, GeneralizedNewtypeDeriving, MultiParamTypeClasses #-}
+{-# LANGUAGE ExistentialQuantification, GeneralizedNewtypeDeriving, MultiParamTypeClasses, ConstraintKinds #-}
 
 {- |
 This module implements the Key/Value types, to abstract over hetrogenous data types.
@@ -6,7 +6,8 @@
 module Development.Shake.Value(
     Value, newValue, fromValue, typeValue,
     Key, newKey, fromKey, typeKey,
-    Witness, currentWitness, registerWitness
+    Witness, currentWitness, registerWitness,
+    ShakeValue
     ) where
 
 import General.Binary
@@ -23,19 +24,52 @@
 import qualified Data.ByteString.Char8 as BS
 import System.IO.Unsafe
 
+-- | Define an alias for the six type classes required for things involved in Shake 'Development.Shake.Rule's.
+--   Using this alias requires the @ConstraintKinds@ extension.
+--
+--   To define your own values meeting the necessary constraints it is convenient to use the extensions
+--   @GeneralizedNewtypeDeriving@ and @DeriveDataTypeable@ to write:
+--
+-- > newtype MyType = MyType (String, Bool) deriving (Show, Typeable, Eq, Hashable, Binary, NFData)
+--
+--   Shake needs these instances on keys and values. They are used for:
+--
+-- * 'Show' is used to print out keys in errors, profiling, progress messages
+--   and diagnostics.
+--
+-- * 'Typeable' is used because Shake indexes its database by the
+--   type of the key and value involved in the rule (overlap is not
+--   allowed for type classes and not allowed in Shake either).
+--
+-- * 'Eq' and 'Hashable' are used on keys in order to build hash maps
+--   from keys to values.  'Eq' is used on values to test if the value
+--   has changed or not (this is used to support unchanging rebuilds,
+--   where Shake can avoid rerunning rules if it runs a dependency,
+--   but it turns out that no changes occurred.)  The 'Hashable'
+--   instances are only use at runtime (never serialised to disk),
+--   so they do not have to be stable across runs.
+--   Hashable on values is not used, and only required for a consistent interface.
+--
+-- * 'Binary' is used to serialize keys and values into Shake's
+--   build database; this lets Shake cache values across runs and
+--   implement unchanging rebuilds.
+--
+-- * 'NFData' is used to avoid space and thunk leaks, especially
+--   when Shake is parallelized.
+type ShakeValue a = (Show a, Typeable a, Eq a, Hashable a, Binary a, NFData a)
 
 -- We deliberately avoid Typeable instances on Key/Value to stop them accidentally
 -- being used inside themselves
 newtype Key = Key Value
     deriving (Eq,Hashable,NFData,BinaryWith Witness)
 
-data Value = forall a . (Eq a, Show a, Typeable a, Hashable a, Binary a, NFData a) => Value a
+data Value = forall a . ShakeValue a => Value a
 
 
-newKey :: (Eq a, Show a, Typeable a, Hashable a, Binary a, NFData a) => a -> Key
+newKey :: ShakeValue a => a -> Key
 newKey = Key . newValue
 
-newValue :: (Eq a, Show a, Typeable a, Hashable a, Binary a, NFData a) => a -> Value
+newValue :: ShakeValue a => a -> Value
 newValue = Value
 
 typeKey :: Key -> TypeRep
@@ -74,7 +108,7 @@
 witness :: IORef (Map.HashMap TypeRep Value)
 witness = unsafePerformIO $ newIORef Map.empty
 
-registerWitness :: (Eq a, Show a, Typeable a, Hashable a, Binary a, NFData a) => a -> IO ()
+registerWitness :: ShakeValue a => a -> IO ()
 registerWitness x = atomicModifyIORef witness $ \mp -> (Map.insert (typeOf x) (Value $ err msg `asTypeOf` x) mp, ())
     where msg = "registerWitness, type " ++ show (typeOf x)
 
diff --git a/src/General/Binary.hs b/src/General/Binary.hs
--- a/src/General/Binary.hs
+++ b/src/General/Binary.hs
@@ -26,9 +26,9 @@
     getWith ctx = do n <- get; replicateM n $ getWith ctx
 
 instance BinaryWith ctx a => BinaryWith ctx (Maybe a) where
-    putWith ctx Nothing = putWord8 0
+    putWith _ Nothing = putWord8 0
     putWith ctx (Just x) = putWord8 1 >> putWith ctx x
-    getWith ctx = do i <- getWord8; if i == 0 then return Nothing else fmap Just $ getWith ctx
+    getWith ctx = do i <- getWord8; if i == 0 then return Nothing else Just <$> getWith ctx
 
 
 newtype BinList a = BinList {fromBinList :: [a]}
@@ -37,13 +37,13 @@
 
 instance Binary a => Binary (BinList a) where
     put (BinList xs) = case splitAt 254 xs of
-        (a, []) -> putWord8 (genericLength xs) >> mapM_ put xs
+        (_, []) -> putWord8 (genericLength xs) >> mapM_ put xs
         (a, b) -> putWord8 255 >> mapM_ put a >> put (BinList b)
     get = do
         x <- getWord8
         case x of
             255 -> do xs <- replicateM 254 get; BinList ys <- get; return $ BinList $ xs ++ ys
-            n -> fmap BinList $ replicateM (fromInteger $ toInteger n) get
+            n -> BinList <$> replicateM (fromInteger $ toInteger n) get
 
 
 newtype BinFloat = BinFloat {fromBinFloat :: Float}
diff --git a/src/General/Cleanup.hs b/src/General/Cleanup.hs
--- a/src/General/Cleanup.hs
+++ b/src/General/Cleanup.hs
@@ -32,7 +32,7 @@
 --   run the action, 'False' to say ignore the action (and never run it).
 addCleanup :: Cleanup -> IO () -> IO (Bool -> IO ())
 addCleanup (Cleanup ref) act = atomicModifyIORef ref $ \s -> let i = unique s in
-    (,) (S (unique s + 1) (Map.insert i act $ items s)) $ \b -> do
+    (,) (S (unique s + 1) (Map.insert i act $ items s)) $ \b ->
         join $ atomicModifyIORef ref $ \s -> case Map.lookup i $ items s of
             Nothing -> (s, return ())
             Just act -> (s{items = Map.delete i $ items s}, when b act)
diff --git a/src/General/Extra.hs b/src/General/Extra.hs
--- a/src/General/Extra.hs
+++ b/src/General/Extra.hs
@@ -1,10 +1,10 @@
-{-# LANGUAGE BangPatterns, CPP #-}
 
 module General.Extra(
     getProcessorCount,
     randomElem,
     showQuote,
     withs,
+    maximum', maximumBy'
     ) where
 
 import Control.Exception.Extra
@@ -14,10 +14,8 @@
 import System.IO.Extra
 import System.IO.Unsafe
 import System.Random
-#if __GLASGOW_HASKELL__ >= 704
 import Control.Concurrent
-import Foreign.C.Types
-#endif
+import GHC.Conc
 
 
 ---------------------------------------------------------------------
@@ -31,23 +29,15 @@
 ---------------------------------------------------------------------
 -- System.Info
 
-#if __GLASGOW_HASKELL__ >= 704
--- Use the underlying GHC function
-foreign import ccall getNumberOfProcessors :: IO CInt
-#endif
-
-
 {-# NOINLINE getProcessorCount #-}
 getProcessorCount :: IO Int
 -- unsafePefromIO so we cache the result and only compute it once
 getProcessorCount = let res = unsafePerformIO act in return res
     where
         act =
-#if __GLASGOW_HASKELL__ >= 704
             if rtsSupportsBoundThreads then
-                fmap fromIntegral $ getNumberOfProcessors
+                fmap fromIntegral $ getNumProcessors
             else
-#endif
                 handle_ (const $ return 1) $ do
                     env <- lookupEnv "NUMBER_OF_PROCESSORS"
                     case env of
@@ -72,3 +62,11 @@
 withs :: [(a -> r) -> r] -> ([a] -> r) -> r
 withs [] act = act []
 withs (f:fs) act = f $ \a -> withs fs $ \as -> act $ a:as
+
+
+-- See https://ghc.haskell.org/trac/ghc/ticket/10830 - they broke maximumBy
+maximumBy' :: (a -> a -> Ordering) -> [a] -> a
+maximumBy' cmp = foldl1' $ \x y -> if cmp x y == GT then x else y
+
+maximum' :: Ord a => [a] -> a
+maximum' = maximumBy' compare
diff --git a/src/General/FileLock.hs b/src/General/FileLock.hs
new file mode 100644
--- /dev/null
+++ b/src/General/FileLock.hs
@@ -0,0 +1,76 @@
+{-# LANGUAGE CPP, ScopedTypeVariables #-}
+
+module General.FileLock(withLockFile) where
+
+import Control.Exception.Extra
+import System.Directory
+import System.FilePath
+#ifdef mingw32_HOST_OS
+import Data.Bits
+import Data.Word
+import Foreign.Ptr
+import Foreign.C.Types
+import Foreign.C.String
+#else
+import System.IO
+import System.Posix.IO
+#endif
+
+#ifdef mingw32_HOST_OS
+
+#ifdef x86_64_HOST_ARCH
+#define CALLCONV ccall
+#else
+#define CALLCONV stdcall
+#endif
+
+foreign import CALLCONV unsafe "Windows.h CreateFileW" c_CreateFileW :: Ptr CWchar -> Word32 -> Word32 -> Ptr () -> Word32 -> Word32 -> Ptr () -> IO (Ptr ())
+foreign import CALLCONV unsafe "Windows.h CloseHandle" c_CloseHandle :: Ptr () -> IO Bool
+foreign import CALLCONV unsafe "Windows.h GetLastError" c_GetLastError :: IO Word32
+
+c_GENERIC_WRITE = 0x40000000 :: Word32
+c_GENERIC_READ  = 0x80000000 :: Word32
+c_FILE_SHARE_NONE = 0 :: Word32
+c_OPEN_ALWAYS = 4 :: Word32
+c_FILE_ATTRIBUTE_NORMAL = 0x80 :: Word32
+c_INVALID_HANDLE_VALUE = intPtrToPtr (-1)
+c_ERROR_SHARING_VIOLATION = 32
+#endif
+
+withLockFile :: FilePath -> IO a -> IO a
+
+#ifdef mingw32_HOST_OS
+
+withLockFile file act = withCWString file $ \cfile -> do
+    createDirectoryIfMissing True $ takeDirectory file
+    let open = c_CreateFileW cfile (c_GENERIC_READ .|. c_GENERIC_WRITE) c_FILE_SHARE_NONE nullPtr c_OPEN_ALWAYS c_FILE_ATTRIBUTE_NORMAL nullPtr
+    bracket open c_CloseHandle $ \h ->
+        if h == c_INVALID_HANDLE_VALUE then do
+            err <- c_GetLastError
+            errorIO $ "Shake failed to acquire a file lock on " ++ file ++ "\n" ++
+                      (if err == c_ERROR_SHARING_VIOLATION
+                          then "ERROR_SHARING_VIOLATION - Shake is probably already running."
+                          else "Code " ++ show err ++ ", unknown reason for failure.")
+        else
+            act
+
+#else
+
+withLockFile file act = do
+    createDirectoryIfMissing True $ takeDirectory file
+    try $ writeFile file "" :: IO (Either IOException ())
+
+    bracket (openFd file ReadWrite Nothing defaultFileFlags) closeFd $ \fd -> do
+        let lock = (WriteLock, AbsoluteSeek, 0, 0)
+        res <- try $ setLock fd lock
+        case res of
+            Right () -> act
+            Left (e :: IOException) -> do
+                res <- getLock fd lock
+                errorIO $ "Shake failed to acquire a file lock on " ++ file ++ "\n" ++
+                          (case res of
+                               Nothing -> ""
+                               Just (pid, _) -> "Shake process ID " ++ show pid ++ " is using this lock.\n") ++
+                          show e
+
+#endif
diff --git a/src/General/Intern.hs b/src/General/Intern.hs
--- a/src/General/Intern.hs
+++ b/src/General/Intern.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE GeneralizedNewtypeDeriving, FlexibleInstances, MultiParamTypeClasses #-}
 
 module General.Intern(
-    Intern, Id,
+    Intern, Id(..),
     empty, insert, add, lookup, toList, fromList
     ) where
 
diff --git a/src/General/Process.hs b/src/General/Process.hs
--- a/src/General/Process.hs
+++ b/src/General/Process.hs
@@ -3,7 +3,7 @@
 -- | A wrapping of createProcess to provide a more flexible interface.
 module General.Process(
     Buffer, newBuffer, readBuffer,
-    process, ProcessOpts(..), Destination(..)
+    process, ProcessOpts(..), Source(..), Destination(..)
     ) where
 
 import Control.Applicative
@@ -49,6 +49,11 @@
 ---------------------------------------------------------------------
 -- OPTIONS
 
+data Source
+    = SrcFile FilePath
+    | SrcString String
+    | SrcBytes LBS.ByteString
+
 data Destination
     = DestEcho
     | DestFile FilePath
@@ -64,7 +69,7 @@
     ,poCwd :: Maybe FilePath
     ,poEnv :: Maybe [(String, String)]
     ,poTimeout :: Maybe Double
-    ,poStdin :: Either String LBS.ByteString
+    ,poStdin :: [Source]
     ,poStdout :: [Destination]
     ,poStderr :: [Destination]
     ,poAsync :: Bool
@@ -84,11 +89,15 @@
 stdStream file _ _ = CreatePipe
 
 
-stdIn :: Either String LBS.ByteString -> (StdStream, Handle -> IO ())
-stdIn inp | either null LBS.null inp = (Inherit, const $ return ())
-          | otherwise = (,) CreatePipe $ \h ->
+stdIn :: (FilePath -> Handle) -> [Source] -> (StdStream, Handle -> IO ())
+stdIn file [] = (Inherit, const $ return ())
+stdIn file [SrcFile x] = (UseHandle $ file x, const $ return ())
+stdIn file src = (,) CreatePipe $ \h ->
     void $ tryBool isPipeGone $ do
-        either (hPutStr h) (LBS.hPutStr h) inp
+        forM_ src $ \x -> case x of
+            SrcString x -> hPutStr h x
+            SrcBytes x -> LBS.hPutStr h x
+            SrcFile x -> LBS.hPutStr h =<< LBS.hGetContents (file x)
         hFlush h
         hClose h
     where
@@ -133,17 +142,22 @@
             void $ waitForProcess pid
 
 
+withFiles :: IOMode -> [FilePath] -> ((FilePath -> Handle) -> IO a) -> IO a
+withFiles mode files act = withs (map (`withFile` mode) files) $ \handles ->
+    act $ \x -> fromJust $ lookup x $ zip files handles
+
+
 -- General approach taken from readProcessWithExitCode
 process :: ProcessOpts -> IO (ProcessHandle, ExitCode)
 process po = do
     (ProcessOpts{..}, flushBuffers) <- optimiseBuffers po
-    let files = nubOrd [x | DestFile x <- poStdout ++ poStderr]
-    withs (map (`withFile` WriteMode) files) $ \handles -> do
-        let fileHandle x = fromJust $ lookup x $ zip files handles
+    let outFiles = nubOrd [x | DestFile x <- poStdout ++ poStderr]
+    let inFiles = nubOrd [x | SrcFile x <- poStdin]
+    withFiles WriteMode outFiles $ \outHandle -> withFiles ReadMode inFiles $ \inHandle -> do
         let cp = (cmdSpec poCommand){cwd = poCwd, env = poEnv, create_group = isJust poTimeout, close_fds = True
-                 ,std_in = fst $ stdIn poStdin
-                 ,std_out = stdStream fileHandle poStdout poStderr, std_err = stdStream fileHandle poStderr poStdout}
-        withCreateProcess cp $ \(inh, outh, errh, pid) -> do
+                 ,std_in = fst $ stdIn inHandle poStdin
+                 ,std_out = stdStream outHandle poStdout poStderr, std_err = stdStream outHandle poStderr poStdout}
+        withCreateProcess cp $ \(inh, outh, errh, pid) ->
             withTimeout poTimeout (abort pid) $ do
 
                 let streams = [(outh, stdout, poStdout) | Just outh <- [outh], CreatePipe <- [std_out cp]] ++
@@ -162,7 +176,7 @@
                         hSetBinaryMode h True
                         dest <- return $ for dest $ \d -> case d of
                             DestEcho -> BS.hPut hh
-                            DestFile x -> BS.hPut (fileHandle x)
+                            DestFile x -> BS.hPut (outHandle x)
                             DestString x -> addBuffer x . (if isWindows then replace "\r\n" "\n" else id) . BS.unpack
                             DestBytes x -> addBuffer x
                         forkWait $ whileM $ do
@@ -172,7 +186,7 @@
                      else if isTied then do
                         dest <- return $ for dest $ \d -> case d of
                             DestEcho -> hPutStrLn hh
-                            DestFile x -> hPutStrLn (fileHandle x)
+                            DestFile x -> hPutStrLn (outHandle x)
                             DestString x -> addBuffer x . (++ "\n")
                         forkWait $ whileM $
                             ifM (hIsEOF h) (return False) $ do
@@ -184,11 +198,11 @@
                         wait1 <- forkWait $ C.evaluate $ rnf src
                         waits <- forM dest $ \d -> case d of
                             DestEcho -> forkWait $ hPutStr hh src
-                            DestFile x -> forkWait $ hPutStr (fileHandle x) src
+                            DestFile x -> forkWait $ hPutStr (outHandle x) src
                             DestString x -> do addBuffer x src; return $ return ()
                         return $ sequence_ $ wait1 : waits
 
-                whenJust inh $ snd $ stdIn poStdin
+                whenJust inh $ snd $ stdIn inHandle poStdin
                 if poAsync then
                     return (pid, ExitSuccess)
                  else do
diff --git a/src/General/String.hs b/src/General/String.hs
--- a/src/General/String.hs
+++ b/src/General/String.hs
@@ -13,7 +13,7 @@
 
 ---------------------------------------------------------------------
 -- Data.ByteString
--- Mostly because ByteString does not have an NFData instance in older GHC
+-- Mostly because ByteString does not have an NFData instance in GHC 7.4
 
 -- | ASCII ByteString
 newtype BS = BS BS.ByteString
@@ -35,6 +35,8 @@
 instance NFData BSU where
     rnf (BSU x) = x `seq` ()
 
+instance Show BSU where
+    show = unpackU
 
 
 pack :: String -> BS
diff --git a/src/General/Template.hs b/src/General/Template.hs
--- a/src/General/Template.hs
+++ b/src/General/Template.hs
@@ -3,6 +3,7 @@
 module General.Template(runTemplate) where
 
 import System.FilePath.Posix
+import Control.Exception.Extra
 import Control.Monad
 import Control.Monad.IO.Class
 import Data.Char
@@ -37,7 +38,7 @@
 
         asker o@(splitFileName -> ("lib/",x)) = case lookup x libraries of
             Just act -> liftIO $ LBS.readFile =<< act
-            Nothing -> error $ "Template library, unknown library: " ++ o
+            Nothing -> liftIO $ errorIO $ "Template library, unknown library: " ++ o
         asker x = ask x
 
 
diff --git a/src/Paths.hs b/src/Paths.hs
--- a/src/Paths.hs
+++ b/src/Paths.hs
@@ -2,15 +2,15 @@
 
 module Paths_shake where
 
-import Data.Version
+import Data.Version.Extra
 import System.IO.Unsafe
 import System.Directory
 import Control.Exception
-import Text.ParserCombinators.ReadP
 
 
 -- We want getDataFileName to be relative to the current directory even if
 -- we issue a change directory command. Therefore, first call caches, future ones read.
+{-# NOINLINE curdir #-}
 curdir :: String
 curdir = unsafePerformIO getCurrentDirectory
 
@@ -20,5 +20,4 @@
     return $ curdir ++ "/" ++ x
 
 version :: Version
--- can't write a literal Version value since in GHC 7.10 the versionsTag field is deprecated
-version = head $ [v | (v,"") <- readP_to_S parseVersion "0.0"] ++ error "version, failed to parse"
+version = makeVersion [0,0]
diff --git a/src/Run.hs b/src/Run.hs
--- a/src/Run.hs
+++ b/src/Run.hs
@@ -6,7 +6,8 @@
 import System.Environment
 import Development.Shake
 import Development.Shake.FilePath
-import General.Timing
+import General.Timing(resetTimings)
+import Control.Applicative
 import Control.Monad.Extra
 import Control.Exception.Extra
 import Data.Maybe
@@ -14,6 +15,7 @@
 import System.Console.GetOpt
 import System.Process
 import System.Exit
+import Prelude
 
 
 main :: IO ()
@@ -29,7 +31,7 @@
                 if takeExtension file `elem` [".hs",".lhs"] then ("runhaskell", file:args) else (toNative file, args)
             e <- rawSystem prog args
             when (e /= ExitSuccess) $ exitWith e
-        Nothing -> do
+        Nothing -> 
             withArgs ("--no-time":args) $
                 shakeArgsWith shakeOptions{shakeCreationCheck=False} flags $ \opts targets -> do
                     let tool = listToMaybe [x | Tool x <- opts]
@@ -43,7 +45,7 @@
                     case () of
                         _ | takeExtension makefile == ".ninja" -> runNinja makefile targets tool
                         _ | isJust tool -> error "--tool flag is not supported without a .ninja Makefile"
-                        _ -> fmap Just $ runMakefile makefile targets
+                        _ -> Just <$> runMakefile makefile targets
 
 
 data Flag = UseMakefile FilePath
diff --git a/src/Test.hs b/src/Test.hs
--- a/src/Test.hs
+++ b/src/Test.hs
@@ -1,6 +1,7 @@
 
 module Test(main) where
 
+import Control.Applicative
 import Control.Exception
 import Control.Monad
 import Data.Maybe
@@ -11,6 +12,7 @@
 import qualified Data.ByteString.Char8 as BS
 import Test.Type(sleepFileTimeCalibrate)
 import Control.Concurrent
+import Prelude
 
 import qualified Test.Assume as Assume
 import qualified Test.Basic as Basic
@@ -23,9 +25,11 @@
 import qualified Test.Directory as Directory
 import qualified Test.Docs as Docs
 import qualified Test.Errors as Errors
+import qualified Test.FileLock as FileLock
 import qualified Test.Files as Files
 import qualified Test.FilePath as FilePath
 import qualified Test.FilePattern as FilePattern
+import qualified Test.Forward as Forward
 import qualified Test.Journal as Journal
 import qualified Test.Lint as Lint
 import qualified Test.Live as Live
@@ -35,13 +39,13 @@
 import qualified Test.Ninja as Ninja
 import qualified Test.Oracle as Oracle
 import qualified Test.OrderOnly as OrderOnly
+import qualified Test.Parallel as Parallel
 import qualified Test.Pool as Pool
 import qualified Test.Progress as Progress
 import qualified Test.Random as Random
 import qualified Test.Resources as Resources
 import qualified Test.Self as Self
 import qualified Test.Tar as Tar
-import qualified Test.Throttle as Throttle
 import qualified Test.Tup as Tup
 import qualified Test.Unicode as Unicode
 import qualified Test.Util as Util
@@ -58,12 +62,13 @@
         ,"basic" * Basic.main, "cache" * Cache.main, "command" * Command.main
         ,"config" * Config.main, "digest" * Digest.main, "directory" * Directory.main
         ,"docs" * Docs.main, "errors" * Errors.main, "orderonly" * OrderOnly.main
-        ,"filepath" * FilePath.main, "filepattern" * FilePattern.main, "files" * Files.main
+        ,"filepath" * FilePath.main, "filepattern" * FilePattern.main, "files" * Files.main, "filelock" * FileLock.main
+        ,"forward" * Forward.main
         ,"journal" * Journal.main, "lint" * Lint.main, "live" * Live.main, "makefile" * Makefile.main, "manual" * Manual.main
-        ,"monad" * Monad.main, "pool" * Pool.main, "random" * Random.main, "ninja" * Ninja.main
+        ,"monad" * Monad.main, "parallel" * Parallel.main, "pool" * Pool.main, "random" * Random.main, "ninja" * Ninja.main
         ,"resources" * Resources.main, "assume" * Assume.main, "benchmark" * Benchmark.main
         ,"oracle" * Oracle.main, "progress" * Progress.main, "unicode" * Unicode.main, "util" * Util.main
-        ,"throttle" * Throttle.main, "verbosity" * Verbosity.main, "version" * Version.main, "tup" * Tup.main]
+        ,"verbosity" * Verbosity.main, "version" * Version.main, "tup" * Tup.main]
     where (*) = (,)
 
 
@@ -104,7 +109,7 @@
     args <- getArgs
     addTiming "Reading files"
     files <- fmap concat $ forM (drop 1 args) $ \file ->
-        fmap (BS.lines . BS.filter (/= '\r')) $ BS.readFile file
+        (BS.lines . BS.filter (/= '\r')) <$> BS.readFile file
     let n = length files
     evaluate n
     addTiming "Modtime"
diff --git a/src/Test/Assume.hs b/src/Test/Assume.hs
--- a/src/Test/Assume.hs
+++ b/src/Test/Assume.hs
@@ -7,7 +7,7 @@
 import Development.Shake.FilePath
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     want $ map obj args
     obj "*.out" %> \out -> do
         cs <- mapM (readFile' . obj . (:".src")) $ takeBaseName out
diff --git a/src/Test/Basic.hs b/src/Test/Basic.hs
--- a/src/Test/Basic.hs
+++ b/src/Test/Basic.hs
@@ -2,6 +2,7 @@
 module Test.Basic(main) where
 
 import Development.Shake
+import System.FilePath
 import Test.Type
 import System.Directory as IO
 import Data.List
@@ -28,26 +29,33 @@
         src <- readFile' $ obj "zero.txt"
         writeFile' out src
 
-    phonys $ \x -> if x /= "halfclean" then Nothing else Just $ do
+    phonys $ \x -> if x /= "halfclean" then Nothing else Just $
         removeFilesAfter (obj "") ["//*e.txt"]
 
-    phony "cleaner" $ do
+    phony "cleaner" $
         removeFilesAfter (obj "") ["//*"]
 
-    phony (obj "configure") $ do
+    phony (obj "configure") $
         liftIO $ appendFile (obj "configure") "1"
 
     phony "install" $ do
         need [obj "configure",obj "once.txt"]
         liftIO $ appendFile (obj "install") "1"
 
-    phony "dummy" $ do
+    phony "duplicate1" $ need ["duplicate2","duplicate3"]
+    phony "duplicate2" $ need ["duplicate3"]
+    phony "duplicate3" $ liftIO $ appendFile (obj "duplicate") "1"
+
+    phony "dummy" $
         liftIO $ appendFile (obj "dummy") "1"
 
     phony "threads" $ do
         x <- getShakeOptions
         writeFile' (obj "threads.txt") $ show $ shakeThreads x
 
+    phony ("slash" </> "platform") $ return ()
+    phony ("slash/forward") $ return ()
+
     obj "dummer.txt" %> \out -> do
         need ["dummy","dummy"]
         need ["dummy"]
@@ -61,6 +69,15 @@
         trace "]"
         writeFile' out out
 
+    obj "sep" </> "1.txt" %> \out -> writeFile' out ""
+    obj "sep/2.txt" %> \out -> writeFile' out ""
+    [obj "sep" </> "3.txt", obj "sep" </> "4.txt", obj "sep" </> "5.*", obj "sep/6.txt"] |%> \out -> writeFile' out ""
+    [obj "sep" </> "7.txt"] |%> \out -> writeFile' out ""
+
+    obj "ids/source" %> \out -> return ()
+    obj "ids/out" %> \out -> do need =<< readFileLines (obj "ids/source"); writeFile' out ""
+    obj "ids/*" %> \out -> do alwaysRerun; trace (takeFileName out); writeFile' out $ takeFileName out
+
 test build obj = do
     writeFile (obj "A.txt") "AAA"
     writeFile (obj "B.txt") "BBB"
@@ -142,5 +159,23 @@
     assertContents (obj "threads.txt") "3"
     build ["!threads","-j0"]
     assertContents (obj "threads.txt") (show processors)
+
+    writeFile (obj "duplicate") ""
+    build ["!duplicate1","!duplicate3"]
+    assertContents (obj "duplicate") "1"
+
+    build $ concat [["sep/" ++ show i ++ ".txt", "sep" </> show i ++ ".txt"] | i <- [1..7]]
+
+    build ["!slash" </> "platform","!slash" </> "forward"]
+    build ["!slash/platform","!slash/forward"]
+
+    createDirectoryIfMissing True (obj "ids")
+    writeFile (obj "ids/source") (obj "ids/a")
+    build ["ids/out","--sleep"]
+    writeFile (obj ".log") ""
+    writeFile (obj "ids/source") (obj "ids/b")
+    build ["ids/out","-j4"]
+    -- if you collapse depends to [Id] then this ends up asking for the stale 'a'
+    assertContents (obj ".log") "b"
 
     build [] -- should say "no want/action statements, nothing to do" (checked manually)
diff --git a/src/Test/Benchmark.hs b/src/Test/Benchmark.hs
--- a/src/Test/Benchmark.hs
+++ b/src/Test/Benchmark.hs
@@ -8,7 +8,7 @@
 
 
 -- | Given a breadth and depth come up with a set of build files
-main = shaken (\_ _ -> return ()) $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     let get ty = head $ [read $ drop (length ty + 1) a | a <- args, (ty ++ "=") `isPrefixOf` a] ++
                         error ("Could not find argument, expected " ++ ty ++ "=Number")
         depth = get "depth"
@@ -19,3 +19,9 @@
         let d = read $ takeBaseName out
         need [obj $ show (d + 1) ++ "." ++ show i | d < depth, i <- [1..breadth]]
         writeFile' out ""
+
+test build obj = do
+    -- these help to test the stack limit
+    build ["clean"]
+    build ["breadth=75","depth=75"]
+    build ["breadth=75","depth=75"]
diff --git a/src/Test/Cache.hs b/src/Test/Cache.hs
--- a/src/Test/Cache.hs
+++ b/src/Test/Cache.hs
@@ -7,7 +7,7 @@
 import Test.Type
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     want $ map obj args
     vowels <- newCache $ \file -> do
         src <- readFile' file
diff --git a/src/Test/Command.hs b/src/Test/Command.hs
--- a/src/Test/Command.hs
+++ b/src/Test/Command.hs
@@ -21,10 +21,11 @@
 import Prelude
 
 
-main = shaken test $ \args obj -> do
-    let helper = [toNative $ obj "shake_helper" <.> exe]
+main = shakenCwd test $ \args obj -> do
+    -- shake_helper must be in a subdirectory so we can test placing that subdir on the $PATH
+    let helper = toNative $ obj "helper/shake_helper" <.> exe
     let name !> test = do want [name | null args || name `elem` args]
-                          name ~> do need [obj "shake_helper" <.> exe]; test
+                          name ~> do need [obj "helper/shake_helper" <.> exe]; test
 
     let helper_source = unlines
             ["import Control.Concurrent"
@@ -50,8 +51,12 @@
             ,"        hFlush stderr"
             ]
 
-    obj "shake_helper.hs" %> \out -> do need ["src/Test/Command.hs"]; writeFileChanged out helper_source
-    obj "shake_helper" <.> exe %> \out -> do need [obj "shake_helper.hs"]; cmd (Cwd $ obj "") "ghc --make" "shake_helper.hs -o shake_helper"
+    obj "shake_helper.hs" %> \out -> do
+        need ["../../src/Test/Command.hs"]
+        writeFileChanged out helper_source
+    [obj "helper/shake_helper" <.> exe, obj "shake_helper.o", obj "shake_helper.hi"] &%> \_ -> do
+        need [obj "shake_helper.hs"]
+        cmd (Cwd $ obj "") "ghc --make" "shake_helper.hs -o helper/shake_helper"
 
     "capture" !> do
         (Stderr err, Stdout out) <- cmd helper ["ostuff goes here","eother stuff here"]
@@ -69,11 +74,13 @@
 
     "cwd" !> do
         -- FIXME: Linux searches the Cwd argument for the file, Windows searches getCurrentDirectory
-        helper <- liftIO $ canonicalizePath $ obj "shake_helper" <.> exe
+        helper <- liftIO $ canonicalizePath $ obj "helper/shake_helper" <.> exe
         Stdout out <- cmd (Cwd $ obj "") helper "c"
-        liftIO $ (===) (trim out) =<< canonicalizePath (dropTrailingPathSeparator $ obj "")
+        let norm = fmap dropTrailingPathSeparator . canonicalizePath . trim
+        liftIO $ join $ liftM2 (===) (norm out) (norm $ obj "")
 
     "timeout" !> do
+        opts <- getShakeOptions
         offset <- liftIO offsetTime
         Exit exit <- cmd (Timeout 2) helper "w20"
         t <- liftIO offset
@@ -88,10 +95,22 @@
         Stdout out <- cmd (AddEnv "FOO" "GOODBYE SHAKE") Shell helper "vFOO"
         liftIO $ out === "GOODBYE SHAKE\n"
 
+    "space" !> do
+        Stdout out <- cmd helper ["oSPACE 1"]
+        liftIO $ out === "SPACE 1\n"
+        Stdout out <- cmd Shell helper "\"oSPACE 2\""
+        liftIO $ out === "SPACE 2\n"
+        whenM (liftIO hasTracker) $ do
+            Stdout out <- cmd Shell AutoDeps helper "\"oSPACE 2\""
+            liftIO $ out === "SPACE 2\n"
+        (Stdout (), CmdLine x) <- cmd helper ["oSPACE 3","oDIRECT"]
+        unless (" \"oSPACE 3\" oDIRECT" `isSuffixOf` replace "\'" "\"" x) $
+            fail $ "Invalid CmdLine, " ++ x
+
     "path" !> do
-        let path = AddPath [dropTrailingPathSeparator $ obj ""] []
-        unit $ cmd $ obj "shake_helper"
-        unit $ cmd $ obj "shake_helper" <.> exe
+        let path = AddPath [dropTrailingPathSeparator $ obj "helper"] []
+        unit $ cmd $ obj "helper/shake_helper"
+        unit $ cmd $ obj "helper/shake_helper" <.> exe
         unit $ cmd path Shell "shake_helper"
         unit $ cmd path "shake_helper"
 
@@ -122,8 +141,10 @@
             [s ++ " = " ++ showDuration d | (s,d) <- [("String",t1),("ByteString",t2),("File",t3)]]
 
     "stdin" !> do
-        Stdout (x :: String) <- cmd helper "i" (Stdin "hello ") (StdinBS $ LBS.pack "world")
-        liftIO $ x === "hello world"
+        withTempFile $ \file -> do
+            liftIO $ writeFile file " "
+            Stdout (x :: String) <- cmd helper "i" (Stdin "hello") (FileStdin file) (StdinBS $ LBS.pack "world")
+            liftIO $ x === "hello world"
 
     "async" !> do
         let file = obj "async.txt"
@@ -135,6 +156,9 @@
 
 test build obj = do
     -- reduce the overhead by running all the tests in parallel
+    -- lint can make a big different to the command lines, so test with and without
+    whenM hasTracker $
+        build ["-j4","--no-lint"]
     build ["-j4"]
 
 
diff --git a/src/Test/Config.hs b/src/Test/Config.hs
--- a/src/Test/Config.hs
+++ b/src/Test/Config.hs
@@ -11,7 +11,7 @@
 import System.Directory
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     want $ map obj ["hsflags.var","cflags.var","none.var","keys"]
     usingConfigFile $ obj "config"
     obj "*.var" %> \out -> do
diff --git a/src/Test/Digest.hs b/src/Test/Digest.hs
--- a/src/Test/Digest.hs
+++ b/src/Test/Digest.hs
@@ -6,7 +6,7 @@
 import Test.Type
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     if null args then want [obj "Out.txt",obj "Out2.txt"] else want $ map obj args
 
     obj "Out.txt" %> \out -> do
@@ -18,6 +18,11 @@
         liftIO $ appendFile out1 txt
         liftIO $ appendFile out2 txt
 
+    [obj "Bug1.txt",obj "Bug2.txt"] &%> \[out1,out2] -> do
+        need [obj "Bug3.txt"]
+        writeFile' out1 "X"
+        writeFile' out2 "Y"
+
     "leaf" ~> return ()
     obj "node1.txt" %> \file -> do need ["leaf"]; writeFile' file "x"
     obj "node2.txt" %> \file -> do need [obj "node1.txt"]; liftIO $ appendFile file "x"
@@ -26,7 +31,7 @@
 test build obj = do
     let outs = take 1 $ map obj ["Out.txt","Out1.txt","Out2.txt"]
     let writeOut x = forM_ outs $ \out -> writeFile out x
-    let writeIn x = writeFile (obj "In.txt") x
+    let writeIn = writeFile (obj "In.txt")
     let assertOut x = forM_ outs $ \out -> assertContents out x
 
     writeOut ""
@@ -86,3 +91,9 @@
         writeFile (obj "node2.txt") "y"
         replicateM_ 2 $ build $ ["node2.txt","--sleep"] ++ [flag | flag /= ""]
         assertContents (obj "node2.txt") $ 'y' : replicate count 'x'
+
+    -- test for #296
+    writeFile (obj "Bug3.txt") "X"
+    build ["--digest-and-input","Bug1.txt","--sleep"]
+    writeFile (obj "Bug3.txt") "Y"
+    build ["--digest-and-input","Bug1.txt","--lint"]
diff --git a/src/Test/Directory.hs b/src/Test/Directory.hs
--- a/src/Test/Directory.hs
+++ b/src/Test/Directory.hs
@@ -5,9 +5,11 @@
 import Development.Shake.FilePath
 import Test.Type
 import Data.List
+import Data.Function
 import Control.Monad
 import System.Directory(getCurrentDirectory, setCurrentDirectory, createDirectory, createDirectoryIfMissing)
 import qualified System.Directory as IO
+import qualified System.IO.Extra as IO
 
 
 -- Use escape characters, _o=* _l=/ __=<space>
@@ -24,7 +26,8 @@
           f x = [x]
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
+    let unobj = id
     want $ map obj args
     obj "*.contents" %> \out ->
         writeFileLines out =<< getDirectoryContents (obj $ readEsc $ dropExtension $ unobj out)
@@ -40,7 +43,7 @@
         fs <- mapM doesFileExist xs
         ds <- mapM doesDirectoryExist xs
         let bool x = if x then "1" else "0"
-        writeFileLines out $ zipWith (\a b -> bool a ++ bool b) fs ds
+        writeFileLines out $ zipWith ((++) `on` bool) fs ds
 
     obj "dots" %> \out -> do
         cwd <- liftIO getCurrentDirectory
@@ -74,14 +77,19 @@
     demand " *.txt //*.xtx.files" "A.txt B.txt C.txt/E.xtx"
     demand " C.txt/*.files" "C.txt/D.txt C.txt/E.xtx"
 
+    demand " missing_dir/*.files" ""
+    demand " missing_dir/bar/*.files" ""
+    demand " //missing_dir/*.files" ""
+    assertException ["missing_dir","does not exist"] $ build ["--quiet",showEsc "missing_dir *.files"]
+
     build ["dots","--no-lint"]
     assertContents (obj "dots") $ unlines $ words "True True True True True"
 
-    let removeTest pat del keep = do
-            withTemporaryDirectory $ \dir -> do
+    let removeTest pat del keep =
+            IO.withTempDir $ \dir -> do
                 forM_ (del ++ keep) $ \s -> do
                     createDirectoryIfMissing True $ dir </> takeDirectory s
-                    when (not $ hasTrailingPathSeparator s) $
+                    unless (hasTrailingPathSeparator s) $
                         writeFile (dir </> s) ""
                 removeFiles dir pat
                 createDirectoryIfMissing True dir
@@ -98,13 +106,13 @@
     removeTest ["baz"] ["baz"] ["foo","bar/bob"]
     removeTest ["baz"] ["baz/bob","baz/"] ["foo","bar/bob"]
     removeTest ["Foo//*"] ["Foo/bar","Foo/Quux/bar","Foo/Quux/"] []
-    removeTest ["Foo//*"] ["Foo/"] ["bar"]
+    removeTest ["Foo//"] ["Foo/"] ["bar"]
     removeTest ["baz"] [] ["test.hs","bar/","foo/"]
     removeTest ["bob//*"] [] ["test/bob/"]
-    removeTest ["//bob"] ["test/bob/","test/"] []
-    removeTest ["//*.txt"] ["more/","more/a.txt"] []
-    removeTest ["//*.txt"] ["more/","more/a.txt/"] []
-    removeTest ["//*.txt"] ["more/","more/a.txt/","more/b.txt"] []
+    removeTest ["//bob"] ["test/bob/"] ["test/"]
+    removeTest ["//*.txt"] ["more/a.txt"] ["more/"]
+    removeTest ["//*.txt"] ["more/a.txt/"] ["more/"]
+    removeTest ["//*.txt"] ["more/a.txt/","more/b.txt"] ["more/"]
     removeTest ["//*.txt"] [] ["more/"]
     removeTest ["a//b"] ["a/c/b"] []
     removeFiles "non-existing-directory" ["*"]
diff --git a/src/Test/Docs.hs b/src/Test/Docs.hs
--- a/src/Test/Docs.hs
+++ b/src/Test/Docs.hs
@@ -1,16 +1,22 @@
-{-# LANGUAGE PatternGuards #-}
+{-# LANGUAGE PatternGuards, ViewPatterns #-}
 
 module Test.Docs(main) where
 
 import Development.Shake
 import Development.Shake.FilePath
 import Test.Type
+import Control.Monad
 import Data.Char
 import Data.List.Extra
 import Data.Maybe
+import System.Info
+import Data.Version.Extra
 
 
-main = shaken noTest $ \args obj -> do
+-- Older versions of Haddock garbage the --@ markup
+brokenHaddock = compilerVersion < makeVersion [7,8]
+
+main = shaken (\a b -> unless brokenHaddock $ noTest a b) $ \args obj -> do
     let index = obj "dist/doc/html/shake/index.html"
     let config = obj "dist/setup-config"
     want [obj "Success.txt"]
@@ -20,18 +26,22 @@
     let needSource = need =<< getDirectoryFiles "." ["src/Development/Shake.hs","src/Development/Shake//*.hs","src/Development/Ninja/*.hs","src/General//*.hs"]
 
     config %> \_ -> do
-        need ["shake.cabal"]
-        unit $ cmd "runhaskell Setup.hs configure" ["--builddir=" ++ obj "dist","--user"]
+        need ["shake.cabal","Setup.hs"]
+        -- Make Cabal and Stack play nicely
+        path <- getEnv "GHC_PACKAGE_PATH"
+        unit $ cmd (RemEnv "GHC_PACKAGE_PATH") "runhaskell Setup.hs configure"
+            ["--builddir=" ++ obj "dist","--user"]
+            -- package-db is very sensitive, see #267
+            ["--package-db=" ++ x | x <- maybe [] (filter (`notElem` [".",""]) . splitSearchPath) path]
         trackAllow [obj "dist//*"]
 
     index %> \_ -> do
-        need [config,"shake.cabal"]
+        need [config,"shake.cabal","Setup.hs","README.md","CHANGES.txt"]
         needSource
-        need ["shake.cabal"]
         trackAllow [obj "dist//*"]
         cmd "runhaskell Setup.hs haddock" ["--builddir=" ++ obj "dist"]
 
-    obj "Paths_shake.hs" %> \out -> do
+    obj "Paths_shake.hs" %> \out ->
         copyFile' "src/Paths.hs" out
 
     obj "Part_*.hs" %> \out -> do
@@ -41,29 +51,26 @@
             fmap (findCodeMarkdown . lines . noR) $ readFile' $ "docs/" ++ drop 5 (reverse (drop 3 $ reverse $ takeBaseName out)) ++ ".md"
          else
             fmap (findCodeHaddock . noR) $ readFile' $ obj $ "dist/doc/html/shake/" ++ replace "_" "-" (drop 5 $ takeBaseName out) ++ ".html"
-        let f i (Stmt x) | any whitelist x = []
-                         | otherwise = restmt i $ map undefDots $ trims x
-            f i (Expr x) | takeWhile (not . isSpace) x `elem` types = ["type Expr_" ++ show i ++ " = " ++ x]
-                         | "import " `isPrefixOf` x = [x]
-                         | otherwise = ["expr_" ++ show i ++ " = (" ++ undefDots x2 ++ ")" | let x2 = trim $ dropComment x, not $ whitelist x2]
-            code = concat $ zipWith f [1..] (nubOrd src)
-            (imports,rest) = partition ("import " `isPrefixOf`) code
-        writeFileLines out $
+
+        let (imports,rest) = partition ("import " `isPrefixOf`) $ showCode src
+        writeFileChanged out $ unlines $
             ["{-# LANGUAGE DeriveDataTypeable, RankNTypes, MultiParamTypeClasses, ExtendedDefaultRules, GeneralizedNewtypeDeriving #-}"
-            ,"{-# LANGUAGE NoMonomorphismRestriction, ScopedTypeVariables #-}"
+            ,"{-# LANGUAGE NoMonomorphismRestriction, ScopedTypeVariables, ConstraintKinds #-}"
             ,"{-# OPTIONS_GHC -w #-}"
             ,"module " ++ takeBaseName out ++ "() where"
             ,"import Control.Applicative"
             ,"import Control.Concurrent"
             ,"import Control.Monad"
+            ,"import Data.ByteString(ByteString)"
             ,"import Data.Char"
             ,"import Data.Data"
-            ,"import Data.List"
+            ,"import Data.List.Extra"
+            ,"import System.Time.Extra"
             ,"import Data.Maybe"
             ,"import Data.Monoid"
-            ,"import Development.Shake"
+            ,"import Development.Shake hiding ((*>))"
             ,"import Development.Shake.Classes"
-            ,"import Development.Shake.Rule"
+            ,"import Development.Shake.Rule hiding (trackAllow)"
             ,"import Development.Shake.Util"
             ,"import Development.Shake.FilePath"
             ,"import System.Console.GetOpt"
@@ -71,6 +78,7 @@
             ,"import qualified System.Directory"
             ,"import System.Process"
             ,"import System.Exit"
+            ,"import Control.Applicative"
             ,"import Control.Monad.IO.Class"
             ,"import System.IO"] ++
             ["import " ++ replace "_" "." (drop 5 $ takeBaseName out) | not $ "_md.hs" `isSuffixOf` out] ++
@@ -96,6 +104,9 @@
             ,"myVariable = ()"
             ,"instance Eq (OptDescr a)"
             ,"(foo,bar,baz) = undefined"
+            ,"xs = []"
+            ,"ys = []"
+            ,"out = \"\""
             ,"str1 = \"\""
             ,"str2 = \"\""
             ,"str = \"\""] ++
@@ -120,160 +131,197 @@
         needModules
         need [obj "Main.hs", obj "Paths_shake.hs"]
         needSource
-        () <- cmd "runhaskell -ignore-package=hashmap " ["-i" ++ obj "","-isrc",obj "Main.hs"]
+        unit $ cmd "runhaskell -ignore-package=hashmap " ["-i" ++ obj "","-isrc",obj "Main.hs"]
         writeFile' out ""
 
 
-data Code = Stmt [String] | Expr String deriving (Show,Eq,Ord)
+---------------------------------------------------------------------
+-- FIND THE CODE
 
+newtype Code = Code [String] deriving (Show,Eq,Ord)
+
+
 findCodeHaddock :: String -> [Code]
-findCodeHaddock x | Just x <- stripPrefix "<pre>" x = f (Stmt . shift . lines . strip) "</pre>" x
-                  | Just x <- stripPrefix "<code>" x = f (Expr . strip) "</code>" x
-    where
-        f ctor end x | Just x <- stripPrefix end x = ctor "" : findCodeHaddock x
-        f ctor end (x:xs) = f (ctor . (x:)) end xs
-findCodeHaddock (x:xs) = findCodeHaddock xs
-findCodeHaddock [] = []
+findCodeHaddock src =
+    [ Code $ unindent $ lines $ innerText x
+    | tag <- ["code","pre"]
+    , x <- insideTag tag src
+    , let bad = nubOrd (insideTag "em" x) \\ italics
+    , if null bad then True else error $ "Bad italics, " ++ show bad]
 
+
 findCodeMarkdown :: [String] -> [Code]
-findCodeMarkdown (x:xs) | indented x && not (blank x) =
-    let (a,b) = span (\x -> indented x || blank x) (x:xs)
-    in Stmt (map (drop 4) a) : findCodeMarkdown b
+findCodeMarkdown (x:xs) | indented x && not (isBlank x) =
+    let (a,b) = span (\x -> indented x || isBlank x) (x:xs)
+    in Code (unindent a) : findCodeMarkdown b
     where
         indented x = length (takeWhile isSpace x) >= 4
-        blank x = all isSpace x
-findCodeMarkdown (x:xs) = f x ++ findCodeMarkdown xs
+findCodeMarkdown (x:xs) = map (Code . return) (evens $ splitOn "`" x) ++ findCodeMarkdown xs
     where
-        f ('`':xs) = let (a,b) = break (== '`') xs in Expr a : f (drop 1 b)
-        f (x:xs) = f xs
-        f [] = []
+        evens (x:y:xs) = y : evens xs
+        evens _ = []
 findCodeMarkdown [] = []
 
-trims = reverse . dropWhile (all isSpace) . reverse . dropWhile (all isSpace)
 
-restmt i ("":xs) = restmt i xs
-restmt i (('-':'-':_):xs) = restmt i xs
-restmt i (x:xs) | " ?== " `isInfixOf` x || " == " `isInfixOf` x =
-    zipWith (\j x -> "hack_" ++ show i ++ "_" ++ show j ++ " = " ++ x) [1..] (x:xs)
-restmt i (x:xs) |
-    not ("let" `isPrefixOf` x) && not ("[" `isPrefixOf` x) && not ("cmd " `isPrefixOf` x) && (" = " `isInfixOf` x || " | " `isInfixOf` x || " :: " `isInfixOf` x) ||
-    "import " `isPrefixOf` x || "infix" `isPrefixOf` x || "instance " `isPrefixOf` x = map f $ x:xs
-    where f x = if takeWhile (not . isSpace) x `elem` dupes then "_" ++ show i ++ "_" ++ x else x
-restmt i xs = ("stmt_" ++ show i ++ " = do") : map ("  " ++) xs ++
-              ["  undefined" | length xs == 1 && ("let" `isPrefixOf` (head xs) || "<-" `isInfixOf` (head xs))]
+---------------------------------------------------------------------
+-- RENDER THE CODE
 
+showCode :: [Code] -> [String]
+showCode = concat . zipWith f [1..] . nubOrd
+    where
+        f i (Code x) | "#" `isPrefixOf` concat x = []
+                     | all whitelist x = []
+                     | otherwise = showStmt i $ filter (not . isBlank . dropComment) $ map (fixCmd . undefDots) x
 
-shift :: [String] -> [String]
-shift xs | all null xs = xs
-         | all (\x -> null x || " " `isPrefixOf` x) xs = shift $ map (drop 1) xs
-         | otherwise = xs
 
+fixCmd :: String -> String
+fixCmd x | "cmd " `isPrefixOf` x || "command " `isPrefixOf` x = "unit $ " ++ x
+fixCmd x = replace "Stdout out" "Stdout (out :: String)" $ replace "Stderr err" "Stderr (err :: String)" x
 
-dropComment ('-':'-':_) = []
-dropComment xs = onTail dropComment xs
+-- | Replace ... with undefined (don't use undefined with cmd; two ...'s should become one replacement)
+undefDots :: String -> String
+undefDots x | Just x <- stripSuffix "..." x, Just (x,_) <- stripInfix "..." x = x ++ new
+            | otherwise = replace "..." new x
+    where new = if words x `disjoint` ["cmd","Development.Shake.cmd"] then "undefined" else "[\"\"]"
 
+showStmt :: Int -> [String] -> [String]
+showStmt i [] = []
+showStmt i xs | isDecl $ unlines xs = map f xs
+    where f x = if fst (word1 x) `elem` dupes then "_" ++ show i ++ "_" ++ x else x
+showStmt i (x:xs) | fst (word1 x) `elem` types = ["type Code_" ++ show i ++ " = " ++ x]
+showStmt i [x] | length (words x) <= 2 = ["code_" ++ show i ++ " = (" ++ x ++ ")"] -- deal with operators and sections
+showStmt i xs | all isPredicate xs, length xs > 1 =
+    zipWith (\j x -> "code_" ++ show i ++ "_" ++ show j ++ " = " ++ x) [1..] xs
+showStmt i xs = ("code_" ++ show i ++ " = do") : map ("  " ++) xs ++ ["  undefined" | isBindStmt $ last xs]
 
-undefDots o = f o
-    where
-        f ('.':'.':'.':xs) =
-            (if any (`elem` words o) ["cmd","Development.Shake.cmd"] then "[\"\"]" else "undefined") ++
-            (if "..." `isSuffixOf` xs then "" else undefDots xs)
-        f xs = onTail f xs
+isPredicate :: String -> Bool
+isPredicate x = not $ disjoint (words x) ["==","?=="]
 
-strip :: String -> String
-strip x
-    | Just x <- stripPrefix "<em>" x
-    , (a,b) <- break (== '<') x
-    , not $ ("</em>" `isPrefixOf` b) && a `elem` italics
-    = error $ "Unexpected italics in code block: " ++ a ++ take 5 b ++ "..."
-strip ('<':xs) = strip $ drop 1 $ dropWhile (/= '>') xs
-strip ('&':xs)
-    | Just xs <- stripPrefix "quot;" xs = '\"' : strip xs
-    | Just xs <- stripPrefix "lt;" xs = '<' : strip xs
-    | Just xs <- stripPrefix "gt;" xs = '>' : strip xs
-    | Just xs <- stripPrefix "amp;" xs = '&' : strip xs
-strip xs = onTail strip xs
+isBindStmt :: String -> Bool
+isBindStmt x = "let " `isPrefixOf` x || " <- " `isInfixOf` x
 
-onTail f (x:xs) = x : f xs
-onTail f [] = []
+isDecl :: String -> Bool
+isDecl x | fst (word1 x) `elem` ["import","infix","instance","newtype"] = True
+isDecl (words -> name:"::":_) | all isAlphaNum name = True -- foo :: Type Signature
+isDecl x | "=" `elem` takeWhile (`notElem` ["let","where"]) (words $ takeWhile (/= '{') x) = True -- foo arg1 arg2 = an implementation
+isDecl _ = False
 
 
+---------------------------------------------------------------------
+-- TEXT MANIPULATION
+
+-- | Is a string empty or whitespace
+isBlank :: String -> Bool
+isBlank = all isSpace
+
+-- | If all lines are indented by at least n spaces, then trim n spaces from each line
+unindent :: [String] -> [String]
+unindent xs = map (drop n) xs
+    where n = minimum $ 1000 : map (length . takeWhile (== ' ')) (filter (not . isBlank) xs)
+
+-- | Remove line comments from the end of lines
+dropComment :: String -> String
+dropComment = fst . breakOn "--"
+
+-- | Find all pieces of text inside a given tag
+insideTag :: String -> String -> [String]
+insideTag tag = map (fst . breakOn ("</" ++ tag ++ ">")) . drop 1 . splitOn ("<" ++ tag ++ ">")
+
+-- | Given some HTML, find the raw text
+innerText :: String -> String
+innerText ('<':xs) = innerText $ drop 1 $ dropWhile (/= '>') xs
+innerText ('&':xs)
+    | Just xs <- stripPrefix "quot;" xs = '\"' : innerText xs
+    | Just xs <- stripPrefix "lt;" xs = '<' : innerText xs
+    | Just xs <- stripPrefix "gt;" xs = '>' : innerText xs
+    | Just xs <- stripPrefix "amp;" xs = '&' : innerText xs
+innerText (x:xs) = x : innerText xs
+innerText [] = []
+
+
+---------------------------------------------------------------------
+-- DATA SECTION
+
+-- | Only the following identifiers can appear in italic code blocks in Haddock
+--   (otherwise it's a common markup mistake)
 italics :: [String]
-italics = words "extension command-name file-name"
+italics = words "command-name file-name N"
 
+-- | Identifiers that indicate the fragment is a type
+types :: [String]
+types = words $
+    "MVar IO String FilePath Maybe [String] Char ExitCode Change " ++
+    "Action Resource Assume FilePattern Development.Shake.FilePattern " ++
+    "Lint Verbosity Rules CmdOption Int Double " ++
+    "NFData Binary Hashable Eq Typeable Show Applicative " ++
+    "CmdResult ByteString ProcessHandle Rule Monad Monoid Data TypeRep"
+
+-- | Duplicated identifiers which require renaming
+dupes :: [String]
+dupes = words "main progressSimple rules"
+
+
+isFilePath :: String -> Bool
+isFilePath x = all validChar  x && ("foo/" `isPrefixOf` x || takeExtension x `elem` exts)
+    where
+        validChar x = isAlphaNum x || x `elem` "_./*"
+        exts = words $ ".txt .hi .hs .o .exe .tar .cpp .cfg .dep .out .deps .m .h .c .html .zip " ++
+                       ".js .json .trace .database .src .sh .bat .ninja .rot13 .version .digits .prof .md"
+
+isCmdFlag :: String -> Bool
+isCmdFlag "+RTS" = True
+isCmdFlag x = length a >= 1 && length a <= 2 && all (\x -> isAlphaNum x || x `elem` "-=/_[]") b
+    where (a,b) = span (== '-') x
+
+isCmdFlags :: String -> Bool
+isCmdFlags = all (\x -> let y = fromMaybe x $ stripSuffix "," x in isCmdFlag y || isArg y) . words
+    where isArg = all (\x -> isUpper x || x == '=')
+
+isEnvVar :: String -> Bool
+isEnvVar x | Just x <- stripPrefix "$" x = all validChar x
+           | Just x <- stripPrefix "%" x, Just x <- stripSuffix "%" x = all validChar x
+           | otherwise = False
+    where validChar x = isAlpha x || x == '_'
+
+isProgram :: String -> Bool
+isProgram (words -> x:xs) = x `elem` programs && all (\x -> isCmdFlag x || isFilePath x || all isAlpha x || x == "&&") xs
+    where programs = words "excel gcc cl make ghc cabal distcc build tar git fsatrace ninja touch pwd runhaskell rot13 main shake stack"
+
+-- | Should a fragment be whitelisted and not checked
 whitelist :: String -> Bool
-whitelist x | all (not . isSpace) x && takeExtension x `elem` words ".txt .hi .hs .o .exe .tar .cpp .cfg .dep .deps .h .c .html .zip" = True
+whitelist x | null x || isFilePath x || isCmdFlags x || isEnvVar x || isProgram x = True
 whitelist x | elem x $ words $
-    "newtype do MyFile.txt.digits excel a q m c x value key gcc cl os make contents tar ghc cabal clean _make distcc ghc " ++
-    ".. /./ /.. /../ ./ // \\ ../ //*.c //*.txt //* dir/*/* dir " ++
+    "newtype do a q m c x value key os contents clean _make " ++
+    ".. /. // \\ //* dir/*/* dir " ++
     "ConstraintKinds TemplateHaskell GeneralizedNewtypeDeriving DeriveDataTypeable SetConsoleTitle " ++
-    "Data.List System.Directory Development.Shake.FilePath main.m run .rot13 " ++
-    "NoProgress Error src rot13 .js .json .trace about://tracing " ++
-    ".make/i586-linux-gcc/output _make/.database foo/.. file.src file.out build " ++
-    "/usr/special /usr/special/userbinary $CFLAGS %PATH% -O2 -j8 -j -j1 " ++
-    "-threaded -rtsopts -I0 Function extension $OUT $C_LINK_FLAGS $PATH xterm $TERM main opts result flagValues argValues " ++
-    "HEADERS_DIR /path/to/dir CFLAGS let -showincludes -MMD gcc.version linkFlags temp pwd touch code out err " ++
-    "_metadata/.shake.database _shake _shake/build ./build.sh build.sh build.bat [out] manual " ++
-    "docs/manual _build _build/run ninja depfile build.ninja ByteString ProcessHandle " ++
-    "Rule CmdResult ShakeValue Monoid Monad Eq Typeable Data " ++ -- work only with constraint kinds
-    "@ndm_haskell file-name " ++
+    "Data.List System.Directory Development.Shake.FilePath run " ++
+    "NoProgress Error src about://tracing " ++
+    ".make/i586-linux-gcc/output build " ++
+    "/usr/special /usr/special/userbinary " ++
+    "Hidden extension xterm main opts result flagValues argValues " ++
+    "HEADERS_DIR /path/to/dir CFLAGS let linkFlags temp code out err " ++
+    "_shake _shake/build manual chrome://tracing/ compdb " ++
+    "docs/manual foo.* _build _build/run depfile 0.000s " ++
+    "@ndm_haskell file-name .PHONY filepath trim base stack extra #include " ++
     "*> "
     = True
-whitelist x | "Stdout out" `isInfixOf` x || "Stderr err" `isInfixOf` x = True
-whitelist x
-    | "foo/" `isPrefixOf` x -- path examples
-    = True
 whitelist x = x `elem`
     ["[Foo.hi, Foo.o]"
     ,"shake-progress"
-    ,"main -j6"
-    ,"main clean"
     ,"1m25s (15%)"
     ,"3m12s (82%)"
     ,"getPkgVersion $ GhcPkgVersion \"shake\""
     ,"# command-name (for file-name)"
-    ,"ghc --make MyBuildSystem -rtsopts -with-rtsopts=-I0"
-    ,"-with-rtsopts"
-    ,"-qg -qb"
-    ,"gcc -MM"
-    ,"# This is my Config file"
-    ,"-g -I/path/to/dir -O2"
-    ,"main _make/henry.txt"
     ,"<i>build rules</i>"
     ,"<i>actions</i>"
-    ,"() <- cmd ..."
-    ,"x <- inputs"
     ,"shakeFiles=\"_build\""
     ,"#include \""
     ,"pattern %> actions = (pattern ?==) ?> actions" -- because it overlaps
     ,"buildDir = \"_build\""
-    ,"-MMD -MF"
     ,"#!/bin/sh"
-    ,"build _build/main.o"
-    ,"build clean"
-    ,"build -j8"
-    ,"cabal update && cabal install shake"
     ,"shake-build-system"
-    ,"runhaskell Build.hs"
-    ,"runhaskell Build.hs clean"
-    ,"gcc -c main.c -o main.o -MMD -MF main.m"
     ,"\"_build\" </> x -<.> \"o\""
     ,"cmd \"gcc -o\" [out] os"
-    ,"rot13 file.txt -o file.rot13"
-    ,"file.rot13"
-    ,"out -<.> \"txt\""
     ,"[item1,item2,item2]"
-    ,"runhaskell Build.hs"
-    ,"cabal update"
-    ,"cabal install shake"
-    ,"shake -j4"
-    ,"cmd \"gcc -o _make/run _build/main.o _build/constants.o\""
     ,"$(LitE . StringL . loc_filename <$> location)"
     ]
-
-types = words $
-    "MVar IO String FilePath Maybe [String] Char ExitCode Change " ++
-    "Action Resource Assume FilePattern Development.Shake.FilePattern " ++
-    "Lint Verbosity Rules CmdOption Int Double"
-
-dupes = words "main progressSimple rules"
diff --git a/src/Test/Errors.hs b/src/Test/Errors.hs
--- a/src/Test/Errors.hs
+++ b/src/Test/Errors.hs
@@ -12,7 +12,8 @@
 import qualified System.IO.Extra as IO
 
 
-main = shaken2 test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
+    want args
     obj "norule" %> \_ ->
         need [obj "norule_isavailable"]
 
@@ -22,9 +23,12 @@
     [obj "failcreates", obj "failcreates2"] &%> \_ ->
         writeFile' (obj "failcreates") ""
 
-    obj "recursive" %> \out ->
-        need [out]
+    obj "recursive_" %> \out -> need [obj "intermediate_"]
+    obj "intermediate_" %> \out -> need [obj "recursive_"]
 
+    "rec1" %> \out -> need ["rec2"]
+    "rec2" %> \out -> need ["rec1"]
+
     obj "systemcmd" %> \_ ->
         cmd "random_missing_command"
 
@@ -49,7 +53,7 @@
     catcher "exception2" $ actionOnException $ return ()
 
     res <- newResource "resource_name" 1
-    obj "resource" %> \out -> do
+    obj "resource" %> \out ->
         withResource res 1 $
             need ["resource-dep"]
 
@@ -89,65 +93,76 @@
 
     when ("die" `elem` args) $ action $ error "death error"
 
+    obj "fresh_dir" %> \out -> liftIO $ createDirectoryIfMissing True out
+    obj "need_dir" %> \out -> do
+        liftIO $ createDirectoryIfMissing True $ obj "existing_dir"
+        need [obj "existing_dir"]
+        writeFile' out ""
 
+
+    -- not tested by default since only causes an error when idle GC is turned on
+    phony "block" $
+        liftIO $ putStrLn $ let x = x in x
+
 test build obj = do
     let crash args parts = assertException parts (build $ "--quiet" : args)
     build ["clean"]
     build ["--sleep"]
 
     writeFile (obj "chain.1") "x"
-    build ["$chain.3","--sleep"]
+    build ["chain.3","--sleep"]
     writeFile (obj "chain.1") "err"
-    crash ["$chain.3"] ["err_chain"]
+    crash ["chain.3"] ["err_chain"]
 
-    crash ["$norule"] ["norule_isavailable"]
-    crash ["$failcreate"] ["failcreate"]
-    crash ["$failcreates"] ["failcreates"]
-    crash ["$recursive"] ["recursive"]
-    crash ["$systemcmd"] ["systemcmd","random_missing_command"]
-    crash ["$stack1"] ["stack1","stack2","stack3","crash"]
+    crash ["norule"] ["norule_isavailable"]
+    crash ["failcreate"] ["failcreate"]
+    crash ["failcreates"] ["failcreates"]
+    crash ["recursive_"] ["recursive_","intermediate_","recursive"]
+    crash ["rec1","rec2"] ["rec1","rec2","recursive"]
+    crash ["systemcmd"] ["systemcmd","random_missing_command"]
+    crash ["stack1"] ["stack1","stack2","stack3","crash"]
 
     b <- IO.doesFileExist $ obj "staunch1"
     when b $ removeFile $ obj "staunch1"
-    crash ["$staunch1","$staunch2","-j2"] ["crash"]
+    crash ["staunch1","staunch2","-j2"] ["crash"]
     b <- IO.doesFileExist $ obj "staunch1"
     assert (not b) "File should not exist, should have crashed first"
-    crash ["$staunch1","$staunch2","-j2","--keep-going","--silent"] ["crash"]
+    crash ["staunch1","staunch2","-j2","--keep-going","--silent"] ["crash"]
     b <- IO.doesFileExist $ obj "staunch1"
     assert b "File should exist, staunch should have let it be created"
 
-    crash ["$finally1"] ["die"]
+    crash ["finally1"] ["die"]
     assertContents (obj "finally1") "1"
-    build ["$finally2"]
+    build ["finally2"]
     assertContents (obj "finally2") "1"
-    crash ["$exception1"] ["die"]
+    crash ["exception1"] ["die"]
     assertContents (obj "exception1") "1"
-    build ["$exception2"]
+    build ["exception2"]
     assertContents (obj "exception2") "0"
 
     forM_ ["finally3","finally4"] $ \name -> do
-        t <- forkIO $ ignore $ build ['$':name,"--exception"]
+        t <- forkIO $ ignore $ build [name,"--exception"]
         retry 10 $ sleep 0.1 >> assertContents (obj name) "0"
         throwTo t (IndexOutOfBounds "test")
         retry 10 $ sleep 0.1 >> assertContents (obj name) "1"
 
-    crash ["$resource"] ["cannot currently call apply","withResource","resource_name"]
+    crash ["resource"] ["cannot currently call apply","withResource","resource_name"]
 
-    build ["$overlap.foo"]
+    build ["overlap.foo"]
     assertContents (obj "overlap.foo") "overlap.*"
-    build ["$overlap.txt"]
+    build ["overlap.txt"]
     assertContents (obj "overlap.txt") "overlap.txt"
-    crash ["$overlap.txx"] ["key matches multiple rules","overlap.txx"]
-    build ["$alternative.foo","$alternative.txt"]
+    crash ["overlap.txx"] ["key matches multiple rules","overlap.txx"]
+    build ["alternative.foo","alternative.txt"]
     assertContents (obj "alternative.foo") "alternative.*"
     assertContents (obj "alternative.txt") "alternative.txt"
 
-    crash ["$tempfile"] ["tempfile-died"]
+    crash ["tempfile"] ["tempfile-died"]
     src <- readFile $ obj "tempfile"
     assertMissing src
-    build ["$tempdir"]
+    build ["tempdir"]
 
-    crash ["!die"] ["Shake","action","death error"]
+    crash ["die"] ["Shake","action","death error"]
 
     putStrLn "## BUILD errors"
     (out,_) <- IO.captureOutput $ build []
@@ -156,3 +171,6 @@
     putStrLn "## BUILD errors fail1 fail2 -k -j2"
     (out,_) <- IO.captureOutput $ try_ $ build ["fail1","fail2","-k","-j2",""]
     assert ("die1" `isInfixOf` out && "die2" `isInfixOf` out) $ "Expected 'die1' and 'die2', but got: " ++ out
+
+    crash ["fresh_dir"] ["expected a file, got a directory","fresh_dir"]
+    crash ["need_dir"] ["expected a file, got a directory","existing_dir"]
diff --git a/src/Test/FileLock.hs b/src/Test/FileLock.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/FileLock.hs
@@ -0,0 +1,36 @@
+
+module Test.FileLock(main) where
+
+import Development.Shake
+import Control.Concurrent.Extra
+import Control.Exception.Extra
+import Control.Monad
+import Data.Either.Extra
+import System.Time.Extra
+import Test.Type
+
+
+main = shaken test $ \args obj ->
+    action $ do
+        putNormal "Starting sleep"
+        liftIO $ sleep 5
+        putNormal "Finished sleep"
+
+
+test build obj = do
+    -- check it fails exactly once
+    time <- offsetTime
+    lock <- newLock
+    let out msg = do t <- time; withLock lock $ print (t, msg)
+    out "before onceFork"
+    a <- onceFork $ do out "a1"; build ["-VVV"]; out "a2"
+    b <- onceFork $ do out "b1"; build ["-VVV"]; out "b2"
+    out "after onceFork"
+    a <- try_ a
+    out "after try a"
+    b <- try_ b
+    out "after try b"
+    when (length (filter isLeft [a,b]) /= 1) $
+        error $ "Expected one success and one failure, got " ++ show [a,b]
+    -- check it succeeds after the lock has been held
+    build []
diff --git a/src/Test/FilePath.hs b/src/Test/FilePath.hs
--- a/src/Test/FilePath.hs
+++ b/src/Test/FilePath.hs
@@ -13,7 +13,7 @@
 import System.Info.Extra
 
 
-main = shaken test $ \args obj -> return ()
+main = shakenCwd test $ \args obj -> return ()
 
 
 newtype File = File String deriving Show
@@ -61,6 +61,7 @@
     norm "//./" === (if isWindows then "//" else "/")
     norm "//foo/./bar" === (if isWindows then "//foo/bar" else "/foo/bar")
     when isWindows $ norm "c:\\foo\\bar" === "c:/foo/bar"
+    when isWindows $ normaliseEx "foo/bar\\baz" === "foo\\bar\\baz"
     Success{} <- quickCheckWithResult stdArgs{maxSuccess=1000} $ \(File x) ->
         let y = norm x
             sep = Native.isPathSeparator
@@ -72,7 +73,7 @@
                  ,not $ "//" `isInfixOf` noDrive y
                  ,".." `notElem` dropWhile (== "..") (dropWhile (\x -> all isPathSeparator x || isDrive x) $ splitDirectories y)
                  ,norm y == y]
-        in if and ps then True else error $ show (x, y, ps)
+        in if and ps then True else error (show (x, y, ps))
 
     dropDirectory1 "aaa/bbb" === "bbb"
     dropDirectory1 "aaa/" === ""
diff --git a/src/Test/FilePattern.hs b/src/Test/FilePattern.hs
--- a/src/Test/FilePattern.hs
+++ b/src/Test/FilePattern.hs
@@ -3,66 +3,196 @@
 
 import Development.Shake.FilePattern
 import Development.Shake.FilePath
-import Data.Tuple.Extra
+import Control.Monad
+import System.IO.Unsafe
+import System.Info.Extra
+import Data.List.Extra
 import Test.Type
+import Test.QuickCheck hiding ((===))
 
-main = shaken test $ \args obj -> return ()
+main = shakenCwd test $ \args obj -> return ()
 
 
+newtype Pattern = Pattern FilePattern deriving (Show,Eq)
+newtype Path    = Path    FilePath    deriving (Show,Eq)
+
+-- Since / and * are the only "interesting" elements, just add ab to round out the set
+
+instance Arbitrary Pattern where
+    arbitrary = fmap Pattern $ listOf $ elements "\\/*ab"
+    shrink (Pattern x) = map Pattern $ shrinkList (\x -> ['/' | x == '\\']) x
+
+instance Arbitrary Path where
+    arbitrary = fmap Path $ listOf $ elements "\\/ab"
+    shrink (Path x) = map Path $ shrinkList (\x -> ['/' | x == '\\']) x
+
+
 test build obj = do
-    let f b pat file = assert (b == (pat ?== file)) $ show pat ++ " ?== " ++ show file ++ "\nEXPECTED: " ++ show b
+    internalTest
+    let f b pat file = do
+            assert (b == (pat ?== file)) $ show pat ++ " ?== " ++ show file ++ "\nEXPECTED: " ++ show b
+            assert (b == (pat `walker` file)) $ show pat ++ " `walker` " ++ show file ++ "\nEXPECTED: " ++ show b
+            when b $ assert (toStandard (substitute (extract pat file) pat) == toStandard file) $
+                "FAILED substitute/extract property\nPattern: " ++ show pat ++ "\nFile: " ++ show file ++ "\n" ++
+                "Extracted: " ++ show (extract pat file) ++ "\nSubstitute: " ++ show (substitute (extract pat file) pat)
+
     f True "//*.c" "foo/bar/baz.c"
+    f True "**/*.c" "foo/bar/baz.c"
     f True (toNative "//*.c") "foo/bar\\baz.c"
+    f True (toNative "**/*.c") "foo/bar\\baz.c"
     f True "*.c" "baz.c"
     f True "//*.c" "baz.c"
+    f True "**/*.c" "baz.c"
     f True "test.c" "test.c"
     f False "*.c" "foor/bar.c"
     f False "*/*.c" "foo/bar/baz.c"
     f False "foo//bar" "foobar"
+    f False "foo/**/bar" "foobar"
     f False "foo//bar" "foobar/bar"
+    f False "foo/**/bar" "foobar/bar"
     f False "foo//bar" "foo/foobar"
+    f False "foo/**/bar" "foo/foobar"
     f True "foo//bar" "foo/bar"
+    f True "foo/**/bar" "foo/bar"
     f True "foo/bar" (toNative "foo/bar")
     f True (toNative "foo/bar") "foo/bar"
     f True (toNative "foo/bar") (toNative "foo/bar")
+    f True "//*" "/bar"
+    f False "**/*" "/bar"
+    f True "/bob//foo" "/bob/this/test/foo"
+    f True "/bob/**/foo" "/bob/this/test/foo"
+    f False "/bob//foo" "bob/this/test/foo"
+    f False "/bob/**/foo" "bob/this/test/foo"
+    f True "bob//foo/" "bob/this/test/foo/"
+    f True "bob/**/foo/" "bob/this/test/foo/"
+    f False "bob//foo/" "bob/this/test/foo"
+    f False "bob/**/foo/" "bob/this/test/foo"
+    f True "a//" "a"
+    f True "a/**" "a"
+    f True "/a//" "/a"
+    f True "/a/**" "/a"
+    f True "///a//" "/a"
+    f False "**/a/**" "/a"
+    f False "///" ""
+    f True "///" "/"
+    f True "/**" "/"
+    f True "///" "a/"
+    f True "**/" "a/"
+    f True "////" ""
+    f True "**/**" ""
+    f True "x///y" "x/y"
+    f True "x/**/y" "x/y"
+    f True "x///" "x/"
+    f True "x/**/" "x/"
+    f True "x///" "x/foo/"
+    f True "x/**/" "x/foo/"
+    f False "x///" "x"
+    f False "x/**/" "x"
+    f True "x///" "x/foo/bar/"
+    f True "x/**/" "x/foo/bar/"
+    f False "x///" "x/foo/bar"
+    f False "x/**/" "x/foo/bar"
+    f True "x///y" "x/z/y"
+    f True "x/**/*/y" "x/z/y"
+    f True "" ""
+    f False "" "y"
+    f False "" "/"
 
+    f True "*/*" "x/y"
+    f False "*/*" "x"
+    f True "//*" "x"
+    f True "**/*" "x"
+    f True "//*" ""
+    f True "**/*" ""
+    f True "*//" "x"
+    f True "*/**" "x"
+    f True "*//" ""
+    f True "*/**" ""
+    f True "*//*" "x/y"
+    f True "*/**/*" "x/y"
+    f False "*//*" ""
+    f False "*/**/*" ""
+    f False "*//*" "x"
+    f False "*/**/*" "x"
+    f False "*//*//*" "x/y"
+    f False "*/**/*/**/*" "x/y"
+    f True "//*/" "/"
+    f False "**/*/" "/"
+    f True "*/////" "/"
+    f True "*/**/**/" "/"
+    f False "b*b*b*//" "bb"
+    f False "b*b*b*/**" "bb"
+
+    f False "**" "/"
+    f False "**/x" "/x"
+    f True "**" "x/"
+    f (not isWindows) "**" "\\\\drive"
+    f (not isWindows) "**" "C:\\drive"
+    f (not isWindows) "**" "C:drive"
+
+    simple "a*b" === False
+    simple "a//b" === False
+    simple "a/**/b" === False
+    simple "/a/b/cccc_" === True
+    simple "a///b" === False
+    simple "a/**/b" === False
+
     assert (compatible []) "compatible"
     assert (compatible ["//*a.txt","foo//a*.txt"]) "compatible"
+    assert (compatible ["**/*a.txt","foo/**/a*.txt"]) "compatible"
+    assert (compatible ["//*a.txt","foo/**/a*.txt"]) "compatible"
     assert (not $ compatible ["//*a.txt","foo//a*.*txt"]) "compatible"
+    assert (not $ compatible ["**/*a.txt","foo/**/a*.*txt"]) "compatible"
     extract "//*a.txt" "foo/bar/testa.txt" === ["foo/bar/","test"]
+    extract "**/*a.txt" "foo/bar/testa.txt" === ["foo/bar/","test"]
     extract "//*a.txt" "testa.txt" === ["","test"]
+    extract "**/*a.txt" "testa.txt" === ["","test"]
+    extract "//a.txt" "a.txt" === [""]
+    extract "**/a.txt" "a.txt" === [""]
+    extract "//a.txt" "/a.txt" === ["/"]
+    extract "a//b" "a/b" === [""]
+    extract "a/**/b" "a/b" === [""]
+    extract "a//b" "a/x/b" === ["x/"]
+    extract "a/**/b" "a/x/b" === ["x/"]
+    extract "a//b" "a/x/y/b" === ["x/y/"]
+    extract "a/**/b" "a/x/y/b" === ["x/y/"]
+    extract "a///b" "a/x/y/b" === ["x/y/"]
+    extract "a/**/**/b" "a/x/y/b" === ["","x/y/"]
     extract "//*a*.txt" "testada.txt" === ["","test","da"]
+    extract "**/*a*.txt" "testada.txt" === ["","test","da"]
     extract (toNative "//*a*.txt") "testada.txt" === ["","test","da"]
+    extract (toNative "**/*a*.txt") "testada.txt" === ["","test","da"]
     substitute ["","test","da"] "//*a*.txt" === "testada.txt"
+    substitute ["","test","da"] "**/*a*.txt" === "testada.txt"
     substitute  ["foo/bar/","test"] "//*a.txt" === "foo/bar/testa.txt"
+    substitute  ["foo/bar/","test"] "**/*a.txt" === "foo/bar/testa.txt"
 
-    directories1 "*.xml" === ("",False)
-    directories1 "//*.xml" === ("",True)
-    directories1 "foo//*.xml" === ("foo",True)
-    first toStandard (directories1 "foo/bar/*.xml") === ("foo/bar",False)
-    directories1 "*/bar/*.xml" === ("",True)
-    directories ["*.xml","//*.c"] === [("",True)]
-    directories ["bar/*.xml","baz//*.c"] === [("bar",False),("baz",True)]
-    directories ["bar/*.xml","baz//*.c"] === [("bar",False),("baz",True)]
+    (False, Walk _) <- return $ walk ["*.xml"]
+    (False, Walk _) <- return $ walk ["//*.xml"]
+    (False, Walk _) <- return $ walk ["**/*.xml"]
+    (False, WalkTo ([], [("foo",Walk _)])) <- return $ walk ["foo//*.xml"]
+    (False, WalkTo ([], [("foo",Walk _)])) <- return $ walk ["foo/**/*.xml"]
+    (False, WalkTo ([], [("foo",WalkTo ([],[("bar",Walk _)]))])) <- return $ walk ["foo/bar/*.xml"]
+    (False, WalkTo (["a"],[("b",WalkTo (["c"],[]))])) <- return $ walk ["a","b/c"]
+    ([], [("foo",WalkTo ([],[("bar",Walk _)]))]) <- let (False, Walk f) = walk ["*/bar/*.xml"] in return $ f ["foo"]
+    (False, WalkTo ([],[("bar",Walk _),("baz",Walk _)])) <- return $ walk ["bar/*.xml","baz//*.c"]
+    (False, WalkTo ([],[("bar",Walk _),("baz",Walk _)])) <- return $ walk ["bar/*.xml","baz/**/*.c"]
+    (False, WalkTo ([], [])) <- return $ walk []
+    (True, Walk _) <- return $ walk ["//"]
+    (True, Walk _) <- return $ walk ["**"]
+    (True, WalkTo _) <- return $ walk [""]
 
+    Success{} <- quickCheckWithResult stdArgs{maxSuccess=1000} $ \(Pattern p) (Path x) ->
+        let b = p ?== x in (if b then property else label "No match") $ unsafePerformIO $ do f b p x; return True
+    return ()
 
----------------------------------------------------------------------
--- LAZY SMALLCHECK PROPERTIES
 
-{-
-newtype Pattern = Pattern FilePattern deriving (Show,Eq)
-newtype Path    = Path    FilePath    deriving (Show,Eq)
-
--- Since / and * are the only "interesting" elements, just add ab to round out the set
-
-instance Serial Pattern where
-    series = cons Pattern >< f
-        where f = cons [] \/ cons (:) >< const (drawnFrom "/*ab") >< f
-
-instance Serial Path where
-    series = cons Path >< f
-        where f = cons [] \/ cons (:) >< const (drawnFrom "/ab") >< f
-
-testSmallCheck = do
-    smallCheck 10 $ \(Pattern p) (Path x) -> p ?== x ==> substitute (extract p x) p == x
--}
+walker :: FilePattern -> FilePath -> Bool
+-- Slight difference of opinion since Walker is always relative to something
+walker a b | isRelativePattern a, not $ isRelativePath b = False
+walker a b = f (split isPathSeparator b) $ snd $ walk [a]
+    where
+        f (x:xs) (Walk op) = f (x:xs) $ WalkTo $ op [x]
+        f [x]    (WalkTo (file, dir)) = x `elem` file
+        f (x:xs) (WalkTo (file, dir)) | Just w <- lookup x dir = f xs w
+        f _ _ = False
diff --git a/src/Test/Files.hs b/src/Test/Files.hs
--- a/src/Test/Files.hs
+++ b/src/Test/Files.hs
@@ -8,7 +8,7 @@
 import Data.List
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     let fun = "@" `elem` args
     let rest = delete "@" args
     want $ map obj $ if null rest then ["even.txt","odd.txt"] else rest
@@ -28,7 +28,7 @@
         writeFile' b "b"
 
     (\x -> let dir = takeDirectory x in
-           if takeFileName dir /= "pred" then Nothing else Just [dir </> "a.txt",dir </> "b.txt"]) &?> \outs -> do
+           if takeFileName dir /= "pred" then Nothing else Just [dir </> "a.txt",dir </> "b.txt"]) &?> \outs ->
         mapM_ (`writeFile'` "") outs
 
 
diff --git a/src/Test/Forward.hs b/src/Test/Forward.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/Forward.hs
@@ -0,0 +1,22 @@
+
+module Test.Forward(main) where
+
+import Development.Shake
+import Development.Shake.Forward
+import Development.Shake.FilePath
+import Control.Monad.Extra
+import Test.Type
+
+main = shaken test $ \args obj -> forwardRule $ do
+    let src = "src/Test/C"
+    cs <- getDirectoryFiles src ["*.c"]
+    os <- forP cs $ \c -> do
+        let o = obj c <.> "o"
+        cache $ cmd "gcc -c" [src </> c] "-o" [o]
+        return o
+    cache $ cmd "gcc -o" [obj "Main" <.> exe] os
+
+test build obj = do
+    whenM hasTracker $ do
+        build ["--forward","--clean"]
+        build ["--forward","-j2"]
diff --git a/src/Test/Journal.hs b/src/Test/Journal.hs
--- a/src/Test/Journal.hs
+++ b/src/Test/Journal.hs
@@ -14,7 +14,7 @@
 rebuilt = unsafePerformIO $ newIORef 0
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     want $ map obj ["a.out","b.out","c.out"]
     obj "*.out" %> \out -> do
         liftIO $ atomicModifyIORef rebuilt $ \a -> (a+1,())
diff --git a/src/Test/Lint.hs b/src/Test/Lint.hs
--- a/src/Test/Lint.hs
+++ b/src/Test/Lint.hs
@@ -36,7 +36,7 @@
         liftIO $ setCurrentDirectory pwd
         writeFile' out ""
 
-    obj "createonce" %> \out -> do
+    obj "createonce" %> \out ->
         writeFile' out "X"
 
     obj "createtwice" %> \out -> do
@@ -56,7 +56,7 @@
         writeFile' (obj "exists") ""
         writeFile' out ""
 
-    obj "gen*" %> \out -> do
+    obj "gen*" %> \out ->
         writeFile' out out
 
     obj "needed1" %> \out -> do
@@ -79,31 +79,34 @@
 
     obj "tracker-source2" %> \out -> copyFile' (obj "tracker-source1") out
     obj "tracker-read1" %> \out -> do
-        access $ toNative (obj "tracker-source1")
+        access $ obj "tracker-source1"
         writeFile' out ""
     obj "tracker-read2" %> \out -> do
-        access $ toNative (obj "tracker-source1")
+        access $ obj "tracker-source1"
         need [obj "tracker-source1"]
         writeFile' out ""
     obj "tracker-read3" %> \out -> do
-        access $ toNative (obj "tracker-source2")
+        access $ obj "tracker-source2"
         need [obj "tracker-source2"]
         writeFile' out ""
 
     obj "tracker-compile.o" %> \out -> do
         need [obj "tracker-source.c", obj "tracker-source.h"]
         cmd "gcc" ["-c", obj "tracker-source.c", "-o", out]
-    where gen t f = unit $ if isWindows
-                           then cmd "cmd /c" ["echo" ++ t ++ " > " ++ f]
-                           else cmd Shell "echo" ["x", ">", f]
+
+    obj "tracker-compile-auto.o" %> \out -> do
+        need [obj "tracker-source.c"]
+        cmd AutoDeps "gcc" ["-c", obj "tracker-source.c", "-o", out]
+
+    where gen t f = unit $ cmd Shell "echo" t ">" (toNative f)
           access f = unit $ if isWindows
-                            then cmd "cmd /c" ["type " ++ f ++ " > nul"]
-                            else cmd Shell "cat" [f, ">/dev/null"]
+                            then cmd Shell "type" (toNative f) "> nul"
+                            else cmd Shell "cat" f "> /dev/null"
 
 
 test build obj = do
     dir <- getCurrentDirectory
-    let crash args parts = do
+    let crash args parts =
             assertException parts (build $ "--quiet" : args)
                 `finally` setCurrentDirectory dir
 
@@ -117,15 +120,15 @@
     crash ["--clean","listing","existance"] ["changed since being depended upon"]
     crash ["needed1"] ["'needed' file required rebuilding"]
     build ["needed2"]
-
-    when False $ whenM hasTracker $ do
+    whenM hasTracker $ do
         writeFile (obj "tracker-source1") ""
         writeFile (obj "tracker-source2") ""
         writeFile (obj "tracker-source.c") "#include <stdio.h>\n#include \"tracker-source.h\"\n"
-        writeFile (obj "tracker-source.h") ""        
+        writeFile (obj "tracker-source.h") ""
         crash ["tracker-write1"] ["not have its creation tracked","lint/tracker-write1","lint/tracker-write1.txt"]
         build ["tracker-write2"]
         crash ["tracker-read1"] ["used but not depended upon","lint/tracker-source1"]
         build ["tracker-read2"]
         crash ["tracker-read3"] ["depended upon after being used","lint/tracker-source2"]
         build ["tracker-compile.o"]
+        build ["tracker-compile-auto.o"]
diff --git a/src/Test/Live.hs b/src/Test/Live.hs
--- a/src/Test/Live.hs
+++ b/src/Test/Live.hs
@@ -5,7 +5,7 @@
 import Test.Type
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     want $ map obj args
 
     obj "foo" %> \ out -> do
diff --git a/src/Test/Monad.hs b/src/Test/Monad.hs
--- a/src/Test/Monad.hs
+++ b/src/Test/Monad.hs
@@ -11,7 +11,7 @@
 import Control.Monad.IO.Class
 
 
-main = shaken test $ \args obj -> return ()
+main = shakenCwd test $ \args obj -> return ()
 
 
 run :: ro -> rw -> RAW ro rw a -> IO a
@@ -32,7 +32,7 @@
         dump 1 "more"
         res <- tryRAW $ withRO (+3) $ do
             dump 4 "more"
-            withRW (++ "x") $ do
+            withRW (++ "x") $
                 dump 4 "morex"
             dump 4 "more"
             return 100
@@ -74,6 +74,9 @@
         captureRAW $ \k -> throwIO Overflow
         return "x"
     res === Left Overflow
+    -- test for GHC bug 11555
+    runRAW 1 "test" (throw Overflow :: RAW Int String ()) $ \res ->
+        either (Left . fromException) Right res === Left (Just Overflow)
 
     -- catch works properly if continuation called multiple times
     ref <- newIORef []
diff --git a/src/Test/Ninja.hs b/src/Test/Ninja.hs
--- a/src/Test/Ninja.hs
+++ b/src/Test/Ninja.hs
@@ -4,6 +4,7 @@
 import Development.Shake
 import qualified Development.Shake.Config as Config
 import System.Directory(copyFile, createDirectoryIfMissing, removeFile)
+import Control.Applicative
 import Control.Monad
 import Test.Type
 import qualified Data.HashMap.Strict as Map
@@ -12,6 +13,7 @@
 import System.IO.Extra
 import qualified Run
 import System.Environment
+import Prelude
 
 
 main = shaken test $ \args obj -> do
@@ -70,7 +72,7 @@
     runFail "-f../../src/Test/Ninja/lint.ninja bad --lint" "not a pre-dependency"
 
     res <- fmap (drop 1 . lines . fst) $ captureOutput $ run "-f../../src/Test/Ninja/compdb.ninja -t compdb cxx @--no-report @--quiet"
-    want <- fmap lines $ readFile "src/Test/Ninja/compdb.output"
+    want <- lines <$> readFile "src/Test/Ninja/compdb.output"
     let eq a b | (a1,'*':a2) <- break (== '*') a = unless (a1 `isPrefixOf` b && a2 `isSuffixOf` b) $ a === b
                | otherwise = a === b
     length want === length res
@@ -95,6 +97,9 @@
 
     run "-f../../src/Test/Ninja/buildseparate.ninja"
     assertContentsWords (obj "buildseparate.txt") "XX"
+
+    run "-f../../src/Test/Ninja/lexical.ninja"
+    assertContentsWords (obj "lexical.txt") "XFoo_BarXXFooX.bar"
 
     when False $ do
         -- currently fails because Shake doesn't match Ninja here
diff --git a/src/Test/Ninja/lexical.ninja b/src/Test/Ninja/lexical.ninja
new file mode 100644
--- /dev/null
+++ b/src/Test/Ninja/lexical.ninja
@@ -0,0 +1,7 @@
+
+rule test.run
+    command = echo ${foo.bar}$foo.bar > $out
+
+build lexical.txt: test.run
+    foo.bar = XFoo_BarX
+    foo = XFooX
diff --git a/src/Test/Oracle.hs b/src/Test/Oracle.hs
--- a/src/Test/Oracle.hs
+++ b/src/Test/Oracle.hs
@@ -7,8 +7,8 @@
 import Control.Monad
 
 
-main = shaken test $ \args obj -> do
-    let f name lhs rhs = (,) name $
+main = shakenCwd test $ \args obj -> do
+    let f name lhs rhs = (,) name
             (do addOracle $ \k -> let _ = k `asTypeOf` lhs in return rhs; return ()
             ,let o = obj name ++ ".txt" in do want [o]; o %> \_ -> do v <- askOracleWith lhs rhs; writeFile' o $ show v)
     let tbl = [f "str-bool" "" True
diff --git a/src/Test/OrderOnly.hs b/src/Test/OrderOnly.hs
--- a/src/Test/OrderOnly.hs
+++ b/src/Test/OrderOnly.hs
@@ -7,7 +7,7 @@
 import Control.Exception.Extra
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     want $ map obj args
 
     obj "bar.txt" %> \out -> do
@@ -30,7 +30,7 @@
         orderOnly [obj "intermediate.txt"]
         writeFile' out =<< liftIO (readFile $ obj "intermediate.txt")
 
-    obj "intermediate.txt" %> \out -> do
+    obj "intermediate.txt" %> \out ->
         copyFile' (obj "source.txt") out
 
 
diff --git a/src/Test/Parallel.hs b/src/Test/Parallel.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/Parallel.hs
@@ -0,0 +1,42 @@
+
+module Test.Parallel(main) where
+
+import Development.Shake
+import Test.Type
+import Data.Tuple.Extra
+import Control.Monad
+import Control.Concurrent.Extra
+import Data.IORef
+
+
+main = shakenCwd test $ \args obj -> do
+    want args
+
+    phony "cancel" $ do
+        writeFile' (obj "cancel") ""
+        done <- liftIO $ newIORef 0
+        lock <- liftIO newLock
+        void $ parallel $ replicate 5 $ liftIO $ do
+            x <- atomicModifyIORef done $ dupe . succ
+            when (x == 3) $ do sleep 0.1; fail "boom"
+            withLock lock $ appendFile (obj "cancel") "x"
+
+    phony "parallel" $ do
+        active <- liftIO $ newIORef 0
+        peak <- liftIO $ newIORef 0    
+        void $ parallel $ replicate 8 $ liftIO $ do
+            now <- atomicModifyIORef active $ dupe . succ
+            atomicModifyIORef peak $ dupe . max now
+            sleep 0.1
+            atomicModifyIORef active $ dupe . pred
+        peak <- liftIO $ readIORef peak
+        writeFile' (obj "parallel") $ show peak
+
+
+test build obj = do
+    assertException ["boom"] $ build ["cancel","-j1","--quiet"]
+    assertContents (obj "cancel") "xx"
+    build ["parallel","-j1"]
+    assertContents (obj "parallel") "1"
+    build ["parallel","-j5"]
+    assertContents (obj "parallel") "5"
diff --git a/src/Test/Pool.hs b/src/Test/Pool.hs
--- a/src/Test/Pool.hs
+++ b/src/Test/Pool.hs
@@ -9,7 +9,7 @@
 import Control.Monad
 
 
-main = shaken test $ \args obj -> return ()
+main = shakenCwd test $ \args obj -> return ()
 
 
 test build obj = do
@@ -55,7 +55,7 @@
         thread <- newEmptyMVar
         done <- newEmptyMVar
         res <- newMVar True
-        t <- forkIO $ finally (putMVar done ()) $ runPool deterministic 1 $ \pool ->
+        t <- forkIO $ flip finally (putMVar done ()) $ runPool deterministic 1 $ \pool ->
             addPool pool $ do
                 t <- takeMVar thread
                 killThread t
diff --git a/src/Test/Progress.hs b/src/Test/Progress.hs
--- a/src/Test/Progress.hs
+++ b/src/Test/Progress.hs
@@ -1,5 +1,3 @@
-{-# LANGUAGE DeriveDataTypeable #-}
-
 module Test.Progress(main) where
 
 import Development.Shake.Progress
@@ -48,7 +46,7 @@
     -- desirable properties, could be weakened
     xs <- progEx 2 $ 100:map (*2) [10,9..1]
     drop 5 xs === [6,5..1]
-    xs <- progEx 1 $ [10,9,100,8,7,6,5,4,3,2,1]
+    xs <- progEx 1 [10,9,100,8,7,6,5,4,3,2,1]
     assert (all (<= 1.5) $ map abs $ zipWith (-) (drop 5 xs) [6,5..1]) "Close"
 
     -- if no progress is made, don't keep the time going up
diff --git a/src/Test/Random.hs b/src/Test/Random.hs
--- a/src/Test/Random.hs
+++ b/src/Test/Random.hs
@@ -5,6 +5,7 @@
 import Development.Shake
 import Numeric.Extra
 import Test.Type
+import Control.Applicative
 import Control.Exception.Extra
 import Control.Monad
 import Data.List
@@ -15,6 +16,7 @@
 import General.Extra
 import qualified System.IO.Extra as IO
 import System.Time.Extra
+import Prelude
 
 
 inputRange = [1..10]
@@ -138,4 +140,4 @@
                 ns <- randomRIO (0,3)
                 replicateM ns $ randomElem avail
             let r = Logic i xs
-            fmap (r:) $ f (i-1) (Output i:avail)
+            (r:) <$> f (i-1) (Output i:avail)
diff --git a/src/Test/Resources.hs b/src/Test/Resources.hs
--- a/src/Test/Resources.hs
+++ b/src/Test/Resources.hs
@@ -3,31 +3,77 @@
 
 import Development.Shake
 import Test.Type
+import Data.List
+import System.FilePath
+import Control.Exception.Extra hiding (assert)
+import System.Time.Extra
 import Control.Monad
 import Data.IORef
 
 
-main extra = do
-    let cap = 2
+main = shakenCwd test $ \args obj -> do
+    -- test I have good Ord and Show
+    want args
+    do
+        r1 <- newResource "test" 2
+        r2 <- newResource "special" 67
+        unless (r1 < r2 || r2 < r1) $ error "Resources should have a good ordering"
+        unless ("special" `isInfixOf` show r2) $ error "Resource should contain their name when shown"
 
-    ref <- newIORef 0
-    flip (shaken test) extra $ \args obj -> do
-        want $ map obj ["file1.txt","file2.txt","file3.txt","file4.txt"]
+    -- test you are capped to a maximum value
+    do
+        let cap = 2
+        inside <- liftIO $ newIORef 0
         res <- newResource "test" cap
-        res2 <- newResource "test" cap
-        unless (res < res2 || res2 < res) $ error "Resources should have a good ordering"
-        obj "*.txt" %> \out ->
+        phony "cap" $ need [obj $ "c_file" ++ show i ++ ".txt" | i <- [1..4]]
+        obj "c_*.txt" %> \out ->
             withResource res 1 $ do
-                old <- liftIO $ atomicModifyIORef ref $ \i -> (i+1,i)
+                old <- liftIO $ atomicModifyIORef inside $ \i -> (i+1,i)
                 when (old >= cap) $ error "Too many resources in use at one time"
                 liftIO $ sleep 0.1
-                liftIO $ atomicModifyIORef ref $ \i -> (i-1,i)
+                liftIO $ atomicModifyIORef inside $ \i -> (i-1,i)
                 writeFile' out ""
 
+    -- test things can still run while you are blocked on a resource
+    do
+        done <- liftIO $ newIORef 0
+        lock <- newResource "lock" 1
+        phony "schedule" $ do
+            need $ map (\x -> obj $ "s_" ++ x) $ "lock1":"done":["free" ++ show i | i <- [1..10]] ++ ["lock2"]
+        obj "s_done" %> \out -> do
+            need [obj "s_lock1",obj "s_lock2"]
+            done <- liftIO $ readIORef done
+            when (done < 10) $ error "Not all managed to schedule while waiting"
+            writeFile' out ""
+        obj "s_lock*" %> \out -> do
+            withResource lock 1 $ liftIO $ sleep 0.5
+            writeFile' out ""
+        obj "s_free*" %> \out -> do
+            liftIO $ atomicModifyIORef done $ \i -> (i+1,())
+            writeFile' out ""
+
+    -- test that throttle works properly
+    do
+        res <- newThrottle "throttle" 2 0.4
+        phony "throttle" $ need $ map obj ["t_file1.1","t_file2.1","t_file3.2","t_file4.1","t_file5.2"]
+        obj "t_*.*" %> \out -> do
+            withResource res (read $ drop 1 $ takeExtension out) $
+                when (takeBaseName out == "t_file3") $ liftIO $ sleep 0.2
+            writeFile' out ""
+
+
 test build obj = do
-    build ["clean"]
-    build ["-j2"]
-    build ["clean"]
-    build ["-j4"]
-    build ["clean"]
-    build ["-j10"]
+    build ["-j2","cap","--clean"]
+    build ["-j4","cap","--clean"]
+    build ["-j10","cap","--clean"]
+    build ["-j2","schedule","--clean"]
+
+    forM_ ["-j1","-j8"] $ \flags ->
+        -- we are sometimes over the window if the machine is "a bit loaded" at some particular time
+        -- therefore we rerun the test three times, and only fail if it fails on all of them
+        retry 3 $ do
+            (s, _) <- duration $ build [flags,"throttle","--no-report","--clean"]
+            -- the 0.1s cap is a guess at an upper bound for how long everything else should take
+            -- and should be raised on slower machines
+            assert (s >= 1.4 && s < 1.6) $
+                "Bad throttling, expected to take 1.4s + computation time (cap of 0.2s), took " ++ show s ++ "s"
diff --git a/src/Test/Self.hs b/src/Test/Self.hs
--- a/src/Test/Self.hs
+++ b/src/Test/Self.hs
@@ -7,10 +7,13 @@
 import Development.Shake.FilePath
 import Test.Type
 
-import Control.Monad
+import Control.Applicative
+import Control.Monad.Extra
 import Data.Char
 import Data.List.Extra
 import System.Info
+import Data.Version.Extra
+import Prelude
 
 
 newtype GhcPkg = GhcPkg () deriving (Show,Typeable,Eq,Hashable,Binary,NFData)
@@ -48,7 +51,7 @@
         dep <- readFileLines $ out -<.> "dep"
         let xs = map (obj . moduleToFile "deps") dep
         need xs
-        ds <- fmap (nubOrd . sort . (++) dep . concat) $ mapM readFileLines xs
+        ds <- nubOrd . sort . (++) dep <$> concatMapM readFileLines xs
         writeFileLines out ds
 
     obj "//*.dep" %> \out -> do
@@ -82,6 +85,7 @@
 cabalBuildDepends :: String -> [String]
 cabalBuildDepends _ = packages ++ ["unix" | os /= "mingw32"]
 
-packages = words $
-    "base transformers binary unordered-containers hashable time old-time bytestring " ++
-    "filepath directory process deepseq random utf8-string extra js-jquery js-flot"
+packages = words
+    ("base transformers binary unordered-containers hashable time bytestring " ++
+     "filepath directory process deepseq random utf8-string extra js-jquery js-flot") ++
+    ["old-time" | compilerVersion < makeVersion [7,6]]
diff --git a/src/Test/Throttle.hs b/src/Test/Throttle.hs
deleted file mode 100644
--- a/src/Test/Throttle.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-
-module Test.Throttle(main) where
-
-import Development.Shake
-import Development.Shake.FilePath
-import Test.Type
-import Control.Exception.Extra hiding (assert)
-import Control.Monad.Extra
-import System.Time.Extra
-
-
-main = shaken test $ \args obj -> do
-    res <- newThrottle "test" 2 0.4
-    want $ map obj ["file1.1","file2.1","file3.2","file4.1","file5.2"]
-    obj "*.*" %> \out -> do
-        withResource res (read $ drop 1 $ takeExtension out) $
-            when (takeBaseName out == "file3") $ liftIO $ sleep 0.2
-        writeFile' out ""
-
-test build obj = do
-    forM_ [[],["-j8"]] $ \flags -> do
-        -- we are sometimes over the window if the machine is "a bit loaded" at some particular time
-        -- therefore we rerun the test three times, and only fail if it fails on all of them
-        retry 3 $ do
-            build ["clean"]
-            (s, _) <- duration $ build ["--no-report"]
-            -- the 0.1s cap is a guess at an upper bound for how long everything else should take
-            -- and should be raised on slower machines
-            assert (s >= 1.4 && s < 1.6) $
-                "Bad throttling, expected to take 1.4s + computation time (cap of 0.2s), took " ++ show s ++ "s"
diff --git a/src/Test/Tup.hs b/src/Test/Tup.hs
--- a/src/Test/Tup.hs
+++ b/src/Test/Tup.hs
@@ -6,7 +6,9 @@
 import Development.Shake.FilePath
 import Development.Shake.Util
 import Test.Type
+import Control.Applicative
 import Data.Maybe
+import Prelude
 
 
 main = shaken noTest $ \args obj -> do
@@ -21,7 +23,7 @@
             let f x | takeExtension x == ".c" = obj $ dir </> x -<.> "o"
                     | takeExtension x == ".a" = obj $ takeBaseName x </> "lib" ++ x
                     | otherwise = error $ "Unknown extension, " ++ x
-            x <- fmap (fromMaybe $ error $ "Missing config key, " ++ key) $ getConfig key
+            x <- fromMaybe (error $ "Missing config key, " ++ key) <$> getConfig key
             return $ map f $ words x
 
     (\x -> x -<.> exe == x) ?> \out -> do
diff --git a/src/Test/Type.hs b/src/Test/Type.hs
--- a/src/Test/Type.hs
+++ b/src/Test/Type.hs
@@ -2,58 +2,69 @@
 module Test.Type(sleep, module Test.Type) where
 
 import Development.Shake hiding (copyFileChanged)
+import Development.Shake.Forward
 import Development.Shake.Rule() -- ensure the module gets imported, and thus tested
 import General.String
+import General.Extra
 import Development.Shake.FileInfo
 import Development.Shake.FilePath
+import Paths_shake
 
-import Control.Applicative
 import Control.Exception.Extra hiding (assert)
 import Control.Monad.Extra
 import Data.List
 import Data.Maybe
 import qualified Data.ByteString as BS
-import System.Directory as IO
+import System.Directory.Extra as IO
 import System.Environment.Extra
 import System.Random
 import System.Console.GetOpt
 import System.IO.Extra as IO
 import System.Time.Extra
-import System.Info.Extra
 import Prelude
 
 
-shaken
+shaken, shakenCwd
     :: (([String] -> IO ()) -> (String -> String) -> IO ())
     -> ([String] -> (String -> String) -> Rules ())
     -> IO ()
     -> IO ()
-shaken test rules sleeper = do
+shaken = shakenEx False
+shakenCwd = shakenEx True
+
+shakenEx
+    :: Bool
+    -> (([String] -> IO ()) -> (String -> String) -> IO ())
+    -> ([String] -> (String -> String) -> Rules ())
+    -> IO ()
+    -> IO ()
+shakenEx changeDir test rules sleeper = do
+    -- my debug getDataFileName (in Paths) uses a cache of the Cwd
+    -- make sure we force the cache before changing directory
+    getDataFileName ""
+
     name:args <- getArgs
     when ("--sleep" `elem` args) sleeper
     putStrLn $ "## BUILD " ++ unwords (name:args)
-    args <- return $ delete "--sleep" args
+    let forward = "--forward" `elem` args
+    args <- return $ args \\ ["--sleep","--forward"]
+    cwd <- getCurrentDirectory
     let out = "output/" ++ name ++ "/"
-    let obj x = if "/" `isPrefixOf` x then init out ++ x else out ++ x
+    let obj x | changeDir = if null x then "." else x
+              | otherwise = if "/" `isPrefixOf` x || null x then init out ++ x else out ++ x
+    let change = if changeDir then withCurrentDirectory out else id
+    let unchange act = do
+            new <- getCurrentDirectory
+            withCurrentDirectory cwd $ do act; createDirectoryIfMissing True new -- to deal with clean
     createDirectoryIfMissing True out
     case args of
         "test":extra -> do
             putStrLn $ "## TESTING " ++ name
             -- if the extra arguments are not --quiet/--loud it's probably going to go wrong
-            test (\args -> withArgs (name:args ++ extra) $ shaken test rules sleeper) obj
+            change $ test (\args -> withArgs (name:args ++ extra) $ unchange $ shakenEx changeDir test rules sleeper) obj
             putStrLn $ "## FINISHED TESTING " ++ name
+
         "clean":_ -> removeDirectoryRecursive out
-{-
-        "lint":args -> do
-            let dbfile = out ++ ".database"
-                tempfile = "output/" ++ name ++ ".database"
-            b <- IO.doesFileExist dbfile
-            when b $ renameFile dbfile tempfile
-            removeDirectoryRecursive out
-            createDirectoryIfMissing True out
-            when b $ renameFile tempfile dbfile
-            shake shakeOptions{shakeFiles=out, shakeLint=True} $ rules args (out++)
--}
 
         "perturb":args -> forever $ do
             del <- removeFilesRandom out
@@ -62,35 +73,34 @@
             shake shakeOptions{shakeFiles=out, shakeThreads=threads, shakeVerbosity=Quiet} $ rules args (out++)
 
         args -> do
+            t <- tracker
             let (_,files,_) = getOpt Permute [] args
-            tracker <- hasTracker
+            opts <- return $ shakeOptions
+                {shakeFiles = obj ""
+                ,shakeReport = [obj "report.html"]}
+            opts <- return $ if forward then forwardOptions opts else opts
+                {shakeLint = Just t
+                ,shakeLintInside = [cwd]
+                ,shakeLintIgnore = map (cwd </>) [".cabal-sandbox//",".stack-work//"]}
             withArgs (args \\ files) $
-                shakeWithClean
-                    (removeDirectoryRecursive out)
-                    (shakeOptions{shakeFiles = out
-                                 ,shakeReport = ["output/" ++ name ++ "/report.html"]
-                                 ,shakeLint = Just $ if tracker then LintTracker else LintBasic
-                                 })
+                change $ shakeWithClean
+                    (unchange $ removeDirectoryRecursive out)
+                    opts
                     -- if you have passed sleep, supress the "no errors" warning
                     (do rules files obj; when ("--sleep" `elem` args) $ action $ return ())
 
 
-shaken2
-    :: (([String] -> IO ()) -> (String -> String) -> IO ())
-    -> ([String] -> (String -> String) -> Rules ())
-    -> IO ()
-    -> IO ()
-shaken2 test rules sleeper = shaken test rules2 sleeper
-    where
-        rules2 args obj = do
-            (objd,args) <- return $ partition ("$" `isPrefixOf`) args
-            (spec,phon) <- return $ partition ("!" `isPrefixOf`) args
-            want $ phon ++ map (obj . tail) objd
-            rules (map tail spec) obj
-
+tracker :: IO Lint
+tracker = do
+  fsatrace <- findExecutable $ "fsatrace" <.> exe
+  return $ if isJust fsatrace
+           then LintFSATrace
+           else LintBasic
 
 hasTracker :: IO Bool
-hasTracker = isJust <$> if isWindows then findExecutable "tracker.exe" else lookupEnv "FSAT"
+hasTracker = do
+  t <- tracker
+  return $ t == LintFSATrace
 
 
 shakeWithClean :: IO () -> ShakeOptions -> Rules () -> IO ()
@@ -157,7 +167,7 @@
     case res of
         Left err -> let s = show err in forM_ parts $ \p ->
             assert (p `isInfixOf` s) $ "Incorrect exception, missing part:\nGOT: " ++ s ++ "\nWANTED: " ++ p
-        Right _ -> error "Expected an exception but succeeded"
+        Right _ -> error $ "Expected an exception containing " ++ show parts ++ ", but succeeded"
 
 
 noTest :: ([String] -> IO ()) -> (String -> String) -> IO ()
@@ -184,8 +194,8 @@
             writeFile file $ show (i,j)
             t2 <- time
             return $ if t1 == t2 then Left $ j+1 else Right ()
-    putStrLn $ "Longest file modification time lag was " ++ show (ceiling (maximum mtimes * 1000)) ++ "ms"
-    return $ sleep $ min 1 $ maximum mtimes * 2
+    putStrLn $ "Longest file modification time lag was " ++ show (ceiling (maximum' mtimes * 1000)) ++ "ms"
+    return $ sleep $ min 1 $ maximum' mtimes * 2
 
 
 removeFilesRandom :: FilePath -> IO Int
@@ -218,19 +228,4 @@
 copyFileChanged old new = do
     good <- IO.doesFileExist new
     good <- if not good then return False else liftM2 (==) (BS.readFile old) (BS.readFile new)
-    when (not good) $ copyFile old new
-
-
-withTemporaryDirectory :: (FilePath -> IO ()) -> IO ()
-withTemporaryDirectory act = do
-    tdir <- getTemporaryDirectory
-    bracket
-        (openTempFile tdir "shake.hs")
-        (removeFile . fst)
-        $ \(file,h) -> do
-            hClose h
-            let dir = file ++ "_"
-            bracket_ (createDirectory dir) (removeDirectoryRecursive dir) (act dir)
-
-skip :: Monad m => a -> m ()
-skip x = return ()
+    unless good $ copyFile old new
diff --git a/src/Test/Unicode.hs b/src/Test/Unicode.hs
--- a/src/Test/Unicode.hs
+++ b/src/Test/Unicode.hs
@@ -18,7 +18,7 @@
 decode [] = []
 
 
-main = shaken test $ \xs obj -> do
+main = shakenCwd test $ \xs obj -> do
     let pre:args = map decode xs
     want $ map obj args
 
diff --git a/src/Test/Util.hs b/src/Test/Util.hs
--- a/src/Test/Util.hs
+++ b/src/Test/Util.hs
@@ -5,7 +5,7 @@
 import Test.Type
 
 
-main = shaken test $ \args obj -> return ()
+main = shakenCwd test $ \args obj -> return ()
 
 
 test build obj = do
diff --git a/src/Test/Verbosity.hs b/src/Test/Verbosity.hs
--- a/src/Test/Verbosity.hs
+++ b/src/Test/Verbosity.hs
@@ -5,12 +5,12 @@
 import Test.Type
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     want $ map obj args
 
     obj "in.txt" %> \out -> do
         a <- getVerbosity
-        b <- withVerbosity Normal $ getVerbosity
+        b <- withVerbosity Normal getVerbosity
         writeFile' out $ unwords $ map show [a,b]
 
     obj "out.txt" %> \out -> do
diff --git a/src/Test/Version.hs b/src/Test/Version.hs
--- a/src/Test/Version.hs
+++ b/src/Test/Version.hs
@@ -5,7 +5,7 @@
 import Test.Type
 
 
-main = shaken test $ \args obj -> do
+main = shakenCwd test $ \args obj -> do
     want [obj "foo.txt"]
     obj "foo.txt" %> \file -> liftIO $ appendFile file "x"
 
