Tired of Make's moronic syntax and rules? Not working on a project that
requires incremental compilation? 😭 when you need to install 2.4
GiB of Ruby dependencies for a simple build system? Want to be able to execute
simple build rules from anywhere in your project tree? Then sla
is for you!
sla is the Simple Little Automator. It's a tiny shell script that invokes
shell functions found in a build.sla
script in your project's root dir. It
is ideal for smaller projects and projects that don't require compilation,
such as scripting languages.
Every task running / build system I've researched is either for a specific language, too large or complicated or uses declarative style build rules. Declarative rules are very limited in their capabilities. It's often not even possible, or very awkward, to do loops. This immediately leads to the creation of helper shell scripts. The build tool then becomes nothing more than a wrapper around those scripts. So why not use shell scripts right away? That's what Sla does.
Using shell scripting has several benefits:
- It's not limited to a single language. Most projects don't just need to compile code. They also need to setup projects, run tests, generate documentation, etc. Interpreted languages don't require incremental builds at all, making most build systems' core functionality useless.
- It's cross-platform, so it will run everywhere.
- It's powerful. You can script basically anything.
- You don't even need Sla installed. You can run build rules directly through the shell. Sla is just a simple convenient wrapper.
- It's easy to call other build systems such as Make through sla, so we don't lose any functionality such as incremental builds.
Additionally, Sla has some benefits of its own:
- It allows you to list the available rules, something Make still can't do properly after 43 years.
- You can include short and long descriptions / help for build rules.
- You can run target rules from anywhere in your project.
- Sla automatically times your builds.
You can install sla
with itself from the repo.
System-wide:
git clone https://github.com/fboender/sla.git
cd sla
sudo install -m 755 sla /usr/local/bin
Just for your user:
git clone https://github.com/fboender/sla.git
cd sla
install -m 755 -D sla ~/.local/bin
For the above to work, you should have ~/.local/bin
in your PATH.
Here's the output of --help
, although there's not much to see:
Usage: sla [-hdv] [--version] <rule> [param [param...]]
Simple Little Automator. Find and execute tasks from a shell script.
Arguments:
-h, --help Show this help message
-d, --debug Show debug information
-v, --verbose Verbose output
-V, --version Show sla version
If no <rule> is specified, lists the available rules.
Use 'sla <rule> --help' to show a rule's full description.
You can use the --verbose
option to show what your rules are executing.
Basically the same as doing set -x
in your script.
Here's an example of running sla
with the test
rule:
~/Projects/my_project/src/llt/ $ sla test
./src/tools.py:25:80: E501 line too long (111 > 79 characters)
Exection of rule 'test' failed with exitcode 2. Duration: 0h 0m 1s
sla
searches up the current path until it finds a build.sla
file, changes
to that directory, sources it in the shell, and executes the requested shell
function. Here's what the build.sla
file for the above 'test' rule might
look like:
~/Projects/my_project/src/llt/ $ cat ../../build.sla
#
# This is a script containing functions that are used as build rules. You can
# use the Simple Little Automator (https://github.com/fboender/sla) to run
# these rules, or you can run them directly in your shell:
#
# $ bash -c ". build.sla && test"
#
clean () {
# Clean artifacts and trash from repo
find ./ -name "*.pyc" -delete
find ./ -name "*.tmp" -delete
}
test () {
# Run some code tests
clean # Depend on 'clean' rule
flake8 --exclude src/llt --ignore=E501 src/*.py
}
As you can see, build rules are just plain old shell functions. Sla used bash, which should be available just about everywhere.
Creating dependencies on other rules is as simple as calling it as a normal shell function.
You can list available rules by simply omitting a rule name:
$ sla
Available rules:
- doc: Generate documentation
- clean: Clean artifacts and trash from repo
- test: Run some code tests
- install: Install the program
Functions that start with an underscore ('_
') are not shown.
Comments at the start of a function / rule definition are parsed as documentation. Take the following rule definition for example:
install () {
# Install sla
# Install sla to $PREFIX (/usr/local by default).
#
# You can specify the prefix with an environment variable:
#
# $ PREFIX=/usr sla install
# Set the prefix
PREFIX=${PREFIX:-/usr/local}
DEST="$PREFIX/bin/sla"
env install -m 755 ./sla "$DEST"
echo "sla installed in $DEST"
}
In this case the first line is the short description. It is shown when listing
the rules. The rest of the comments, up to the empty line, are shown when
requesting a rule's documentation with sla <rule> --help
. For example:
$ sla install --help
install: Install sla
Install sla to $PREFIX (/usr/local by default).
You can specify the prefix with an environment variable:
$ PREFIX=/usr sla install
Since sla
rules are just plain old shell functions, you don't even need
sla
installed to run them!:
~/Projects/my_project $ bash -c ". build.sla && test"
./src/tools.py:25:80: E501 line too long (111 > 79 characters)
That's useful so that people don't need to install the build system you prefer
just to do a make install
.
You can pass values as environment variables:
$ export TARGET=/usr/local
$ sla install # installs in /usr/local
or
TARGET=/usr/local sla install
You can also pass command-line arguments:
sla install /usr/local
These become available as normal positional arguments in a rule / shell function:
install () {
TARGET=$1
}
Creating dependencies is as easy as calling a shell function. In the following
example, the test
rule depends on the lint
rule and the unittest
rule:
lint () {
# Scan code for common problems
flake8 src/*.py
}
unittest () {
# Run all unit tests
cd src/
nosetests
}
test () {
# Run all tests
lint
unittest
}
If a dependency only needs to be executed once (such as clean
rules), just
set a flag in the rule. For example:
clean () {
if [ -z "$CLEANED" ]; then
echo "Cleaning"
# Your cleaning instructions here.
CLEANED=1
fi
}
Sla sets various flags when running. For example, if the user runs sla -v
(verbose), the FLAG_VERBOSE
flag is set to 1
. You can use this in your
build scripts:
if [ "$FLAG_VERBOSE" -eq 1 ]; then
echo "Additional info"
fi
or
[ "$FLAG_VERBOSE" -eq 1 ] && echo "Additional info"
The following flags are set:
FLAG_DEBUG=1
: Show internal sla debugging infoFLAG_VERBOSE=1
: Show build rule execution debugging info
sla
will stop running as soon as a command returns a non-zero exit code. You
can turn this off by wrapping a block of code with set +e
statements:
set +e
# these commands won't cause Sla to stop if their exit codes are != 0
rm nonexisting
set -e
If you have a rule called "install", and you also want to use the install
unix tool to install software, you'll end up with an endless loop, unless you
specify that you want to use the system's install
and not the shell function
you just wrote. You can use env
for that:
install () {
env install -m 755 ./sla "$DEST"
}
To get auto completion for bash, put in your ~/.bashrc
:
# Complete sla rules
_complete_sla_rules()
{
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
comp=$(sla 2>/dev/null | grep "^ -" | sed "s/^ - \([^:]*\).*/\1/")
COMPREPLY=( $(compgen -W "${comp}" -- $cur))
return 0
}
complete -F _complete_sla_rules sla
You can use a tool like entr to automatically execute rules when files change:
$ find ./ -name "*.1.md" | entr sla doc
Converting sec-diff.1.md
Execution of rule 'doc' succeeded with exitcode 0. Duration: 0h 0m 0s
The doc
rule converts a Markdown file to a manual page. entr
watches all
changes to *.1.md
files in the project, and if any of them change, it
executes sla doc
to regenerate all man pages.
sla is licensed under the MIT License:
Copyright (c) 2022 Ferry Boender
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.