Own your code, part 6: The Lantern Build Engine
It's time again for another installment in the own your code series! In the last post, we looked at the git post-receive hook that calls the main git-repo Laminar CI task, which is the core of our Continuous Integration system (which we discussed in the post before that). You can see all the posts in the series so far here.
In this post we're going to travel in the other direction, and look at the build script / task automation engine that I've developed that goes hand-in-hand with the Laminar CI system - though it can and does stand on it's own too.
Introducing the Lantern Build Engine! Finally, after far too long I'm going to formally post here about it.
Originally developed out of a need to automate the boring and repetitive parts of building and packing my assessed coursework (ACWs) at University, the lantern build engine is my personal task automation system. It's written in 100% Bash, and allows tasks to be easily defined like so:
task_dostuff() {
task_begin "Doing a thing";
do_work;
task_end "$?" "Oops, do_work failed!";
task_begin "Doing another thing";
do_hard_work;
task_end "$?" "Yikes! do_hard_work failed.";
}
When the above task is run, Lantern will automatically detect the dustuff
task, since it's a bash function that's prefixed with task_
. The task_begin
and task_end
calls there are 2 other bash functions, which generate pretty output to inform the user that a task is starting or ending. The $?
there grabs the exit code from the last command - and if it fails task_end
will automatically display the provided error message.
Tasks are defined in a build.sh
file, for which Lantern provides a template. Currently, the template file contains some additional logic such as the help text output if no tasks were specified - which is left-over from the time when Lantern was small enough to fit in the same file as the build tasks themselves.
I'm in the process of adding support for the all the logic in the template file, so that I can cut down on the extra boilerplate there even further. After defining your tasks in a copy of the template build file, it's really easy to call them:
./build dostuff
Of course, don't forget to mark the copy of the template file executable with chmod +x ./build
.
The above initial example only scratches the surface of what Lantern can do though. It can easily check to see if a given command is installed with check_command
:
task_go-to-the-moon() {
task_begin "Checking requirements";
check_command git true;
check_command node true;
check_command npm true;
task_end 0;
}
If any of the check_command
calls fail, then an error message is printed and the build terminated.
Work that needs doing in Lantern can be expressed with 3 levels of logical separation: stages, tasks, and subtasks:
task_build-rocket() {
stage_begin "Preparation";
task_begin "Gathering resources";
gather_resources;
task_end "$?" "Failed to gather resources";
task_begin "Hiring engineers";
hire_engineers;
task_end "$?" "Failed to hire engineers";
stage_end "$?";
stage_begin "Building Rocket";
build_rocket --size big --boosters 99;
stage_end "$?";
stage_begin "Launching rocket";
task_begin "Preflight checks";
subtask_begin "Checking fuel";
check_fuel --level full;
subtask_end "$?" "Error: The fuel tank isn't full!";
subtask_begin "Loading snacks";
load_items --type snacks --from warehouse;
subtask_end "$?" "Error: Failed to load snacks!";
task_end "$?";
task_begin "Launching!";
launch --countdown 10;
task_end "$?";
stage_end "$?";
}
Come to think about it, I should probably rename the function prefix from task
to job
. Stages, tasks, and subtasks each look different in the output - so it's down to personal preference as to which one you use and where. Subtasks in particular are best for commands that don't return any output.
Popular services such as [Travis CI]() have a thing where in the build transcript they display the versions of related programs to the build, like this:
$ uname -a
Linux MachineName 5.3.0-19-generic #20-Ubuntu SMP Fri Oct 18 09:04:39 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
$ node --version
v13.0.1
$ npm --version
6.12.1
Lantern provides support for this with the execute
command. Prefixing commands with execute
will cause them to be printed before being executed, just like the above:
task_prepare() {
task_begin "Displaying environment details";
execute uname -a;
execute node --version;
execute npm --version;
task_end "$?";
}
As build tasks get more complicated, it's logical to split them up into multiple tasks that can be called independently and conditionally. Lantern makes this easy too:
task_build() {
task_begin "Building";
# Do build stuff here
task_end "$?";
}
task_deploy() {
task_begin "Deploying";
# Do deploy stuff here
task_end "$?";
}
task_all() {
tasks_run build deploy;
}
The all
task in the above runs both the build
and deploy tasks. In fact, the template build script uses tasks_run
at the very bottom to treat every argument passed to it as a task name, leading to the behaviour described above.
Lantern also provides an array of other useful functions to make expressing build sequences easy, concise, and readable - from custom colours to testing environment variables to see if they exist. It's all fully documented in the README of the project too.
As described 2 posts ago, the git-repo
Laminar CI task (once it's spawned a hologram of itself) currently checks for the existence of a build
or build.sh
executable script in the root of the repository it is running on, and passes ci
as the first and only argument.
This provides easy integration with Lantern, since Lantern build scripts can be called anything we like, and with a tasks_run
call at the bottom as in the template file, we can simply define a ci
Lantern task function that runs all our continuous integration jobs that we need to execute.
If you're interested in trying out Lantern for yourself, check out the repository!
https://gitlab.com/sbrl/lantern-build-engine#lantern-build-engine
Personally, I use it for everything from CI to rapid development environment setup.
This concludes my (epic) series about my git hosting and continuous integration. We've looked at git hosting, and taken a deep dive into integrating it into a continuous integration system, which we've augmented with a bunch of scripts of our own design. The system we've ended up with, while a lot of work to setup, is extremely flexible, allowing for modifications at will (for example, I have a webhook script that's similar to the git post-receive hook, but is designed to receive notifications from GitHub instead of Gitea and queue the git-repo
just the same).
I'll post a series list post soon. After that, I might blog about my personal apt repository that I've setup, which is somewhat related to this.