Writing deps
It’s all very well to run babushka blah
and have it do jobs for you, but the real power is in babushka’s ability to automate whatever chore you want, not just ones that others have thought of already.
Context (or lack of it)
When you’re writing a dep, you don’t have to think about context at all, just one little task in isolation. As long as your requires
are correct, you can leave the overall structure to babushka and just write each little dep separately.
When you invoke the dep, babushka uses the requires
in each dep to assemble a tree of deps and achieve the end goal you’re after.
The other reason to ignore context when writing each dep is that context is coupling. Keeping deps decoupled means they can be more easily re-used in different situations, where the context is probably different.
Writing met?
and meet
The section on how deps work explains the structure of a dep. To see how each section works, let’s build a simple dep to symlink a directory. Code on the left, shell output on the right.
That’s the simplest dep there is: you don’t need to supply a dep block if you have nothing to put in it. Since the met?
and meet
blocks both default to being empty, though, it’s not very useful.
An empty met?
block defaults to true (so that deps that do nothing but require other deps act as passthroughs).
Let’s add a met?
block with a simple check.
This time, the logic we put in the met?
block returned false (because ~/work
didn’t exist). To see when that block is being run, let’s add some logging:
This shows that babushka is running the met?
block twice: first to check, and then again after running meet
(which is empty in this case). This is where meet
comes in: it’s a piece of logic that, when run, should make met?
return true.
Let’s fill in the meet
block to complete the dep.
This is how deps work (detailed here): it’s the work meet
did on the system, not its return value, that matters. Once meet
runs, the met?
check is passing, and we’re done. Without the logging, the final version of our dep looks like this:
Note the clean split between met? and meet: met?
checks, and meet
acts. If you find you’re checking for the presence of some condition in your meet
block, it probably means you’re trying to do too much in a single dep, and you should be splitting it up into smaller ones. Remember, deps are small, self-contained and context-free - the more focused, the better.
Requirements
The next step in defining a larger task is to take the deps, each one a little nugget of functionality, and relate them to each other. Along with met?
and meet
, there’s a third keyword that’s core to the babushka DSL, and that’s requires
. Unlike met? and meet, which accept a block, requires
accepts a list, each item of which names another dep that this one directly depends on.
This allows us to assemble deps into a tree of dependencies, while still ignoring most of the context (see above): from the point of view of any one dep, all we have to worry about are its direct requirements. The shape of the dependency tree is implied by those requirements, and is lazily assembled by babushka as it runs the deps.
There’s no restrictions on what you can require from where: any dep can require any number of other deps. It’s also fine for a given dep to be required from many others – a given dep will only be invoked once per run. Each subsequent time a requirement pulls it in, the cached result of the first run is used.
In fact, many core deps do this extensively. For example, deps defined against a package manager template all require a dep for the package manager itself (e.g. ‘bundler.gem’, and so on, all require ‘rubygems’). If we have a dep pulling in some libraries our project requires, then on OS X, only the first one will cause the ‘homebrew’ dep to be invoked; subsequent deps use the cached value:
$ babushka 'project libs'
project libs {
curl.lib {
homebrew {
binary.homebrew {
repo.homebrew {
} ✓ repo.homebrew
'brew' runs from /usr/local/bin.
} ✓ binary.homebrew
build tools {
xcode tools {
'cc', 'gcc', 'c++', 'g++', 'llvm-gcc', 'llvm-g++', 'clang', 'make', 'ld' & 'libtool' run from /usr/bin.
} ✓ xcode tools
} ✓ build tools
} ✓ homebrew
} ✓ curl.lib
png.lib {
✓ homebrew (cached)
✓ system has libpng-1.5.13 brew
} ✓ png.lib
yaml.lib {
✓ homebrew (cached)
✓ system has libyaml-0.1.4 brew
} ✓ yaml.lib
} ✓ project libs
The only invalid case is a dependency loop, e.g. a -> b -> c -> a. Babushka detects these, by maintaining a dep callstack and checking that each dep it’s about to run isn’t already in the stack. If a loop is detected, babushka will fail with a descriptive message.
In sum, there’s no need to worry about the resulting graph, or run order, or anything like that: just specify each dep’s immediate requirements, and it’ll all come out in the wash.
Dep parameters
As discussed in the section on running deps, it’s nice to think of deps as similar to a method: they wrap up a bit of logic, separating it from the context it’s subsequently used in. In order to truly use and re-use a method, though, it needs to be parameterised: there’s not much point to wrapping logic in a method if we have to repeat it each time the inputs change. So just as a method has parameters, so do deps.
Deps haven’t always had parameters: originally, babushka had a rather unsavoury system of globally shared variables, that at one point even involved method_missing
(we were all young once). Parameters were introduced in Sep 2011, and all that global nonsense is gone now; the relevant version bump has more details on the redesign.
Parameters are added to a dep by naming them as symbols following its name.
dep 'git', :version do
# ...
end
That defines a ‘version’ parameter, which allows us to supply an argument like this:
dep 'a gittish task' do
requires 'git'.with(version: '1.8.0')
end
As well as passing named arguments in a hash, you can pass them positionally. When passing by name, you can supply as many or as few as you like; when passing positionally, you have to supply them all (the arity has to match).
dep 'file linked', :src, :target do
# ...
end
dep 'babushka linked' do
requires 'file linked'.with('./bin/babushka.rb', '~/bin/babushka')
end
Arguments can also be supplied on the commandline when invoking a parameterised dep directly; see running deps.
Parameters are always optional, and lazy. It’s not until a parameter’s value is requested that it’s resolved; if it doesn’t have a value at that point, then babushka will prompt for it. (If --defaults
was used, then the run will fail on an unset parameter.)
Parameters can have default values, which are suggested when prompting and selectable by hitting ‘enter’:
dep 'postgres', :version do
version.default('9.2.4')
end
They also support silent defaults (“bang defaults”), that are used without prompting whenever a value wasn’t supplied:
dep 'nginx.src', :prefix do
prefix.default!('/opt/nginx')
end
The prompt message can be customised (all these settings can be chained):
dep 'nginx.src', :prefix do
prefix.ask("Where should nginx be installed?").default('/opt/nginx')
end
And choices accepted when prompting can be restricted:
dep 'vhost configured', :type do
# Without descriptions
type.choose(:static, :proxy, :unicorn)
# Including descriptions
type.choose(
:static => "A statically served vhost",
:proxy => "A vhost proxied to a custom host/port",
:unicorn => "A vhost proxying to a unicorn via unix socket"
)
end
This explains why dep parameters aren’t just block parameters, like this:
dep 'git', do |version| # This wouldn't work.
# ...
end
Local variables aren’t available in helper methods, which don’t close over the scope they’re defined in. But more importantly, for chained defaults and lazy prompting to work, referencing parameters has to be a method call.
Parameter limitations
Dep parameters do have some limitations, mainly stemming from the fact that they’re Parameter objects wrapping their values, not the values themselves.
In most situations, parameters will convert and compare as strings correctly. For example given a parameter ‘param’, these cases will all work as expected:
param == 'value'
param[/value/]
File.exists?(param)
Regexp.escape(param)
In cases where there is no default conversion, or it’s not the one we expect, though, the param will fail:
# This will fail: the default conversion here is `#to_i`,
# which isn't defined on Parameter.
'value'[arg]
# This will fail too: the key is the param's value, not
# the param itself, so nil is returned.
{'key' => 'val'}[arg]
In those cases, we just need to convert the parameter explicitly:
'value'[arg.to_s]
{'key' => 'val'}[arg.to_s]
Some proxy objects solve this by becoming transparent to the caller via #method_missing
. Doing so could solve these cases, but I think it makes debugging too susprising: it effectively creates an invisible object. I think the occasional bit of explicitness to maintain predictability is the right tradeoff.
Templated Deps
The basic dep, with just requires
, met?
and meet
, is all you need to describe an end goal. But this generic nature of met?
and meet
means just as they’re general purpose, they can lack focus. For example, installing an app using the system’s package manager has a predictable met?
block—check whether the package is present and its binaries are in the path.
A lot of chores are variations on a theme like this, or just too cumbersome to do repeatedly at a low level. So babushka provides a way to write dep templates, or meta deps, that can be reused later. These meta deps allow you to focus the DSL, and make it even more concise.
For example, Babushka ships with a meta dep that knows how to install TextMate bundles, given just the URL. All the actual logic, including the code for met?
and meet
, is wrapped up in the meta dep.
meta :tmbundle do
accepts_value_for :source
template {
requires 'TextMate.app'
def path
'~/Library/Application Support/TextMate/Bundles' / name
end
met? {
path.dir?
}
meet {
path.parent.mkdir
git source, :to => path
shell %Q{osascript -e 'tell app "TextMate" to reload bundles'}
}
}
end
Notice how the contents of the template
block looks like a normal dep. That’s because it is – the meta dep is a factory that produces deps with their met? and meet blocks already set (they can be overridden in the dep, though).
Also notice the item outside the template: accepts_value_for
. This call is actually part of a second DSL on top of which babushka’s real DSL is built. That call means that deps defined against this template have a new DSL method, ‘source’, that can set and return a value:
meta :tmbundle do
accepts_value_for :source
# ...
end
dep 'Rspec.tmbundle'
source 'https://github.com/dchelimsky/rspec-tmbundle.git'
meet {
log "This dep would install from #{source}."
}
end
In fact, this is how the entire babushka DSL is built: met?
, meet
, and so on are defined in just the same way. The only difference is where they’re defined: they’re found on the base template, so they’re available in all deps.
Let’s look again at the rspec dep we defined above, against the ‘tmbundle’ template.
dep 'Rspec.tmbundle'
source 'https://github.com/dchelimsky/rspec-tmbundle.git'
end
Notice there’s no imperative code there at all—just declarations. That’s what the DSL aims for. Instead of saying “do this, then do this, then do this”, the code should say “here’s a description of the problem, now you work it out.” Also notice that there’s no TextMate-specific logic. Adding this extra level of abstraction means all that’s left are the specifics for this TextMate bundle.