Firstly, I don't think there's anything wrong with the current approach. The Shake package itself uses a similar approach for the built in Ninja and Makefile interpreters,
see details about the Makefile interpreter
or the Sake interpreter. As long as processing the Makefile is sufficiently quick not to
matter (which it usually is) then you are basically writing an
interpreter for your custom build syntax using Shake, which works
nicely.
There are a couple of reasons to avoid that approach, for example, if the processing of the Makefile is expensive, or if the Makefile itself has other dependencies. In that situation you can use a combination of newCache
to parse the Makefile once, and addOracle
so rules can depend on the subset of the Makefile they require. However, that approach is tricker to get right, since it basically forces a layer of indirection, and you have to make some assumptions about how generate_rules
works. The ghc-make package uses this technique (see the "makefile" rule and call to newCache
), and the usingConfigFile
function from the Shake package uses the same newCache
/addOracle
pattern.
Sticking with the original approach, there is somewhere slightly different you can put your findMakefiles
step. You can do:
main = shakeArgsWith shakeOptions [] $ \_ -> xs -> do
makefiles <- findMakefiles
let rules = generate_rules makefiles
if null xs then rules else withoutActions rules >> want xs
This has the advantage that you can easily add command line flags to figure out where to find the Makefile, or options for processing it. Both the Ninja and Makefile systems in Shake support that, using --makefile=FILE
to override the default. Even if you don't want to move generate_rules
, you might want to replace shake
with shakeArgs
in your original example, which will provide you with command line handling.