Let's Get Baked
I just made the same mistake I always make; I named a file containing some Rake tasks with the wrong file extension, .rb
instead of .rake
. This is made all the more embarassing because I’m not working on a Rails app. This is the Ruby/Rack app we’re building at Super Good and I’d just written the code that grabbed all the lib/tasks/*.rake
files and loaded them. Old habits die hard.
When I posted about it, Sean Collins pointed me towards Bake, an alternative to Rake created by Samuel Williams. (That sentence had too many links.) There’s nothing wrong with Rake, but the Bake README explains that it improves on Rake in four main ways:
- On demand loading of files following a standard convention.
- This avoid loading all your rake tasks just to execute a single command.
- Better argument handling including support for positional and optional arguments.
- Focused on task execution not dependency resolution. Implementation is simpler and a bit more predictable.
- Canonical structure for integration with gems.
I don’t think that last bullet is relevant to us, but the other features sound cool. I’m always curious to try new things, so let’s try moving our app over to Bake.
The Status Quo
This app doesn’t have very many tasks yet, only these five:
environment
: This one loads the application environment by usingrequire_relative
to loadconfig/environment.rb
, which does all the work of setting up the application.db:create
: This creates the development and test databases if they don’t exist. (For reasons we don’t need to get into, we don’t need this in production.)db:drop
: You’ll never guess what this does. This is configured so you can’t run it withRACK_ENV=production
because we don’t want to drop our production database. We’re weird like that.db:new_migration
: This takes a name argument and creates a new, blank migration indb/migrations
, naming it by attaching a timestamp to the provided argument.db:migrate
: If no arguments are given, this runs all the unrun migrations in the app. If you give it a version, it will migrate the database up or down to that version.
Our Rakefile is super minimal too. It’s just this:
# Load all tasks from the lib/tasks directory.
Dir[File.join(File.dirname(__FILE__), 'lib', 'tasks', '*.rake')].each { |file| load file }
The gives us a convenient mix of tasks that take arguments, don’t take arguments, and even one that takes an optional argument. Additionally, the db:migrate
task (unlike the others) has a dependency on the environment
task. This all makes for a pretty good test of Bake.
The Bake-sics
Bake provides a guide for getting started. The first step is to add it to the Gemfile. The simplest setup would be to define our tasks as methods in a bake.rb
file at the root of our project. Tasks are defined as methods, like this:
# bake.rb
# Add the x and y values together and print the result.
# @parameter x [Integer]
# @parameter y [Integer]
def add(x, y)
puts x + y
end
Bake detects not just the method (making it runnable as bundle exec bake add 3 5
) but also the comments. It parses the string explaining what the task does as well as the types of the arguments, which is uses to perform type coercion for us. That’s handy.
If we want to write methods that aren’t exposed as tasks, we can just define them as private methods. (Defining private methods in your Rake tasks, while sometimes done, could technically caused unwanted side effects. I’ve written about that.)
Rake gives you a list of available (documented) commands with rake -T
. bake list
is the equivalent command, and you can even specify a specific command you want to see the docs for.
❯ bundle exec bake list add
Bake::Registry::BakefileLoader /Users/jardo/Codes/redacted/bake.rb
add x y
Add the x and y coordinate together and print the result.
x [Integer]
y [Integer]
I like it.
Baking Show
We’ve got a small number of tasks, but we want to keep them namespaced and we anticipate adding more tasks (and more namespaces), so we don’t want to define them all right in our bake.rb
. Bake supports this through facilities for integrating it into projects and gems. I’m mostly only going to discuss the project integration, because I’ve not tried out the gem stuff (yet).
The tasks defined in your bake.rb
are considered private to the project/gem. (If we were working on a gem, consumers of the gem wouldn’t be able to access them.) Since our environment
task doesn’t need to be namespaced, let’s put it in there. That looks this:
# Load the application environment
def environment
require_relative "config/environment"
end
We can test this by running bundle exec bake environment
. This just prints “true”, but that’s what we expect. Perfect.
Climate change is serious and is baking the environment enough as it is. Don’t contribute to that any more. Instead bake cookies, quiches, and pot pies (and push for policy changes at all levels of government that will have real impact).
Bake looks for “recipes” in bake/
. A file at bake/frog.rb
that defines a method called ribbit
will create a task called frog:ribbit
. We’ll use that to define the tasks we need in bake/db.rb
. The ones without arguments will look like this:
# Create the development and test databases
def create
# The guts of the task, copied straight out of
# lib/tasks/db.rake go here.
end
# Drop the development and test databases
def drop
# The guts of the task, copied straight out of
# lib/tasks/db.rake go here.
end
Next up, let’s do db:new_migration
, the task with the required argument. In our Rake version, we have to work with Rake’s somewhat esoteric argument handling. (It’s not that bad, but it’s certainly not natural.) This looked something like this:
task :new_migration, [:name] do |_t, args|
# code...
migration_name = args[:name]
if migration_name.nil?
# provide a useful error message and exit
end
# more code...
end
The Bake version is much cleaner. Let’s take a look:
# Create a new migration
# @param name [String] the name of the migration
def new_migration(name)
# code...
# more code...
end
We don’t need to check if the argument was provided or not. We just use the value. If the user didn’t provide it, Bake would give them a reasonable error message:
0.0s error: Bake::Command [ec=0x260] [pid=9638] [2025-07-31 18:51:57 -0700]
| wrong number of arguments (given 0, expected 1)
| ArgumentError: wrong number of arguments
| → bake/db.rb:55 in 'new_migration'
| [stack trace omitted]
This is a reasonable error, but I don’t love it. I bet Bake could be improved to provide much better errors in this situation. Still, it’s an improvement on Rake.
Our final task to migrate takes an optional argument. It’s short, so here’s the entire Rake version:
task :migrate, [:version] => :environment do |_t, args|
Sequel.extension :migration
version = args[:version].to_i if args[:version]
Sequel::Migrator.run(DB, 'db/migrations', target: version)
end
Bake doesn’t seem to support optional positional arguments, only optional keyword arguments. I’m not sure why that is, but it’s not a problem. Edit: This is because it would be impossible to reliably differentiate additional tasks from optional arguments. (Rake doesn’t support keyword arguments at all.)
# Run migrations
# @param version [Integer] the version to migrate to (omit for latest)
def migrate(version: nil)
call "environment"
Sequel.extension :migration
Sequel::Migrator.run(DB, 'db/migrations', target: version)
end
Let’s talk about the differences. Rather than handling the optionality of the argument and the type coercion ourselves, we rely on a combination of regular ol’ Ruby default keyword arguments and Bake’s type coercion. That’s a nice win.
Bake doesn’t provide a facility for expressing task dependencies like Rake does, so we use call
to invoke the tasks this task depends on. (Rake also has an API for programmatically invoking tasks.) If you need to invoke a task with arguments, you can use lookup
:
lookup("frog:speak").call("ribbit")
I like the Bake version. It feels more Ruby-y to me.
Wake and Bake
I wasn’t really thinking about the consequences of the fact that I’ve framed this post as a review of Bake. Should I finish the post off with a summary of my thoughts and a rating out of ten, like a music review?
If you’ve enjoyed Samuel Williams’ previous albums, like falcon, or his collaborations on with the Japanese supergroup, the Ruby commiters team, then you’ll probably find something to enjoy in this record. Williams really comes into his own on the record, developing his music style in new directions while staying true to his sonic roots.
I’m not going to bother trying to decide whether Rake or Bake is better, but I do like the features it adds. The automatic file loading is handy, and I imagine this would come in extremely handy in an ecosystem of gems using Bake.
While annotating the tasks with comments feels really natural, I’d love to see a couple small improvements to how arguments are handled. If there’s no technical reason we can’t use optional positional arguments, it would be nice them supported. Edit: There is a technical reason. Mostly I just think we could (leveraging the annotations) make argument errors really nice.
Most of all, I like that the task files feel like normal Ruby. I’m not anti-DSL, but I love that Bake was trivial to set up and gave me everything I needed in a task runner without making me learn a DSL. Rake’s not going anywhere and I don’t plan on forgetting how to use it, so this doesn’t really save me (or you) any brain-space, but still, I like it.
I’m not going to finish by giving Bake a star rate, but I am going to mess around with it on some future projects. Seems good to me.
GitHub - ioquatix/bake: An alternative to rake, with all the great stuff and a sprinkling of magic dust.
An alternative to rake, with all the great stuff and a sprinkling of magic dust. ...