Hacé makes things like make, but not the same.
Its functionality is mostly derived from using Croupier, a dataflow library.
If you have crystal
and shards
installed, you can do:
git clone git@github.com:ralsina/hace.git
cd hace
shards build --release
cp bin/hace ~/.local/bin # or wherever you want it
Binaries for Linux in amd64 are available in the releases page.
You can use the hace
command to run tasks from a Hacefile.yml
in the current
directory.
$ ./bin/hace --help
hace - hace makes things, like make
Usage:
hace [command] [flags] [arguments]
Commands:
auto Run in auto mode
help [command] Help about any command.
Flags:
-n, --dry-run Don't actually run any commands
-f, --file Read the file named as a Hacefile default: 'Hacefile.yml'
-h, --help Help for this command.
-k, --keep-going Continue as much as possible after an error.
--question Don't run anything, exit 0 if all tasks are up to date, 1 otherwise
-q, --quiet Don't log anything
-B, --always-make Unconditionally run all tasks.
-v, --verbosity Control the logging verbosity, 0 to 5 default: 3
The arguments are task names, and if you don't specify any, the default tasks will execute.
That's easy, right? Well, that's because the complicated bit is the Hacefile 😃
The Hacefile is a YAML file that describes the tasks you want to run. Conceptually it's a lot like a Makefile, but the syntax and semantics are quite different.
Here's a simple example, details to be explained below:
tasks:
foo:
default: true
dependencies:
- bar
commands: |
echo "make foo out of bar" > foo
cat bar >> foo
phony:
phony: true
commands: echo "bat" > bat
A task is a named set of shell commands that are run in order, and they go
under the tasks
toplevel key. For example:
tasks:
foo:
default: true
dependencies:
- bar
commands: |
echo "make foo out of bar" > foo
cat bar >> foo
This defines a task named foo
that depends on bar
and runs two commands.
Because it's marked as default
it will be run if you don't specify any
tasks on the command line.
The commands are a string that can be multiple lines, and they are run in order. If any command fails, the task fails.
Each line should be a valid shell command, and they are run in a shell. The
shell is /bin/sh
on linux and cmd.exe
on windows (please note that
nobody has ever tried this tool on windows AFAIK).
Commands are in fact templates using Jinja syntax, see Variables below for more details and examples.
A task can have zero, one, or multiple outputs. If a task declares it has outputs but fails to create them, it's considered to have failed.
In the example above, there is no explicit outputs
key, so the task has
one output, named like the task itself: foo
.
A task can declare it generates no outputs by tagging itself as phony
:
task2:
dependencies:
- bar
phony: true
commands: |
notify-send "Done: $(cat bar)"
This example shows a notification on screen with the contents of the file
bar
but doesn't actually create any files.
If a task generates multiple outputs, you can declare them like this:
task3:
dependencies:
- bar
outputs:
- baz
- bat
commands: |
echo "This is baz" > baz
echo "This is bat" > bat
This task is called task3
but it generates two files, baz
and bat
.
⚠Warning: If there are two tasks with the same output, Hacé will run them both, and the second one will overwrite the first. This is a bug, and will be fixed.
Dependencies are files. Hacefile will try to run tasks only if a dependency has changed since the last time the task was run.
If a dependency is the output of another task, then that task will run first (if needed).
If a dependency is missing and the Hacefile doesn't describe how to generate it, the task is not ready to run, and there will be an error.
Tasks without dependencies are always considered "out of date" and will always run if you ask for them.
If a task has default
set to true
, it will run when no task is
specified on the command line. You have to set this explicitly if
you want it, otherwise no task will run unless explicitly required.
If a task has always_run
set to true
, it will run even if it's
not out of date. This is useful for tasks that don't have outputs.
Just an ordinary map of environment variables in the env
top
key. The variables will be available to all tasks and you can
expand them using in commands with ${PATH}
or any other usual
shell mechanism.
If you want to unset a variable, set it to null
. If you want
it set to an empty value, use ""
.
env:
FOO: bar
BAZ: null
You can declare variables in the variables
top level key. They are
available to all tasks, which can use them in their commands
using a Jinja template language syntax.
A special variable is self
which is the task itself, so you can
use the task itself to define parts of the commands it contains.
variables:
i: 3
s: "string"
foo:
bar: "bat"
foo: 86
tasks:
foo:
dependencies:
- bar
commands: |
echo "make foo out of {{ foo['bar'] }} at {{ i }}" > foo
cat {{ self["dependencies"][0] }} >> foo
In that example, it's doing cat bar >> foo
because that's in
self["dependencies"]
. This may look a bit confusing but I expect
it will be useful.
Currently the available members of self
are:
- commands: all commands
- dependencies: all dependencies, as an array
- phony: boolean
- default: boolean
- outputs: all outputs, as an array
You can also set variables from the command line. This example sets VAR
to VALUE
:
hace foo VAR=VALUE
See TODO.md for a list of things that are not done yet, as well as things that were considered and decided against (TODON'T 😀)
Main things to consider if you want to contribute:
-
Take tests seriously. When a project is small, it's easy to test everything.
When it's big, it's impossible to test everything. So, start testing early and keep testing often.
-
Take documentation seriously. If you don't document it, it doesn't exist.
- Fork it (https://github.com/ralsina/hace/fork)
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
- Roberto Alsina - creator and maintainer