Hacé

Hacé makes things like make, but not the same.

Its functionality is mostly derived from using Croupier, a dataflow library.

Docs License Release News about Hace

Tests codecov Mutation Tests

Installation

If you have crystal and shards installed, you can do:

git clone [email protected]: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.

Usage

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

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

Tasks

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
    cwd: /tmp
    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 optional cwd key sets the current working directory for the task.

Commands

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.

Outputs

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: You can have two tasks with the same output, but you can't have two tasks with the same name.

⚠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.

Outputs can interpolate variables and environment variables.

Dependencies

Dependencies are files or task names. Hacefile will try to run tasks only if a dependency file has changed since the last time the task was run or if a task dependency itself would 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.

Dependencies can interpolate variables and environment variables.

Dependencies will expand "globs", such as "*", "**" and "?"

Default tasks

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.

Always Run

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.

Environment variables

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, dependencies and outputs with ${PATH}

Any variables in the environment when the Hacefile is loaded will also be in the environment.

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

Variables

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.

⚠️⚠️WARNING⚠️⚠️ These are not environment variables.

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:

You can also set variables from the command line. This example sets VAR to VALUE:

hace foo VAR=VALUE

Variables can interpolate environment variables:

variables:
  dest_dir: "${HOME}/.local/bin"

Because we are using templates you can even do things like this:

tasks:
  foo:
    default: true
    outputs:
      - foo
    dependencies:
      - "*.c"
    commands: |
      {% for dep in self["dependencies"] %} gcc -c {{dep}} {% endfor %}

Development

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:

Contributing

  1. Fork it (https://github.com/ralsina/hace/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors