Project Scripts
23 Feb 2025TL;DR
Try creating a cli
executable in your project that exposes common project tasks that are written in the project’s core language.
This allows better contribution and less single points of failure with pockets of knowledge in the team.
Scene setting
Over time projects accumulate helper scripts to perform various admin tasks.
I’ve historically tried to avoid bash
as much as possible for these scripts because the projects I work on often have teams of people unfamiliar with bash
or its idiosyncrasies.
With this in mind I’ve then gravitated towards Ruby
because I’ve always loved the language and it’s a safer choice in my mind.
Unfortunately I’ve been kidding myself because as much as I love Ruby
it still has the same issue as bash with people not knowing it and also it’s a right pain to make sure people’s environment are set up.
The next logical step is to just use the main project’s language for building up tasks.
This is potentially easier said than done but I’ve seen success with doing it.
As a mobile developer this means using Swift
with SPM
to build out tasks on the iOS side and Kotlin
with gradle
on the backend/mulitplatform parts.
The good
In taking this approach I’ve removed myself from being the single point of failure on maintaining stuff.
This not only means that I don’t have to be on hand to debug things but also opens the door for easier contribution/reuse.
For example with Swift
being the langauge used to write an admin script other people have contributed various tasks with the obvious plus being that the whole team can much more easily adopt and understand what is being done without trying to understand cryptic personal scripts.
Using languages like Swift
/Kotlin
encourages me to write more reusable code than if I was just slinging bash
around.
For example I’d write a Github
client that can be reused rather than being lazy and copy/pasta’ing curl
invocations around with duplicated configuration.
You have the full power of available libraries like type safe serialisation with Codable
or by pulling in something like kotlinx.serialization
.
I can’t even count how many times I’ve written dodgy JSON interpolation in scripts when really I should have just not been lazy and used the right tools for the job.
Debugging is a super power for these scripts even though I might end up cave man debugging (print
) I love having the option to use a debugger and inspect all the things or try changing state on the fly to see what would happen.
Types, types, types… I love types and they are really handy for helping me write safe code.
The bad
Both Swift
and Kotlin
just aren’t that great as scripting languages even though I really want them to be.
This may be a personal lack of competence but when I’m writing scripts I’m looking for super fast feedback, which means I’ll often start just curl
ing things on the command line or opening TextMate
, setting it to bash and hitting ⌘+R
.
With both of these I’m running code straight away with very little ceremony, which I simply can’t reliably do for Swift
or Kotlin
as both pretty much require that I use an IDE to help with types and missed keywords (try
, await
, suspend
…).
This may sound contradictory to Types, types, types...
but at different points in the development process I value different things.
Often when I am just trying things out I’m not very professional and just want to throw code around to see what works before I put on my big developer pants and do the job properly.
Another weakness is forking.
In bash
or Ruby
I can just slap backticks around a command to have it run in a subshell and then collect the result.
It’s just not that simple in Swift
/Kotlin
even when pulling in libraries, which I do.
Approach
I’m not 100% sold on the exact naming/layout but as a reference this is what I set up.
We have a shim at the root of the project called cli
, this file’s job is to essentially cd
into the project that has the tasks and call swift build
followed by running the built exectuable.
It’s a little bit of redirection but it’s certainly easier than expecting people to remember the calling convention.
My other thinking is that if we come to some standarisation that in our projects you just call ./cli
to be presented with all the various admin tasks then it’s just one thing to learn.
With this in place we currently use swift-argument-parser
to build a cli that has various subcommands as an example for some inspiration here’s some top level tasks that we’ve built out
OVERVIEW: A utility for working with the ios repo.
USAGE: cli <subcommand>
OPTIONS:
-h, --help Show help information.
SUBCOMMANDS:
ci Commands the CI pipeline uses
code-gen Regenerate generated code for the app.
collect-debug-info Print information useful for getting help with debugging.
doctor Help diagnose environment set up issues.
firewall Add/Remove firewall rules for simulator
kmp-doctor Update local repository to add KMP files into Xcode
set-marketing-value Create a branch with a commit that updates to the passed version number.
sim Utilities for working with simulators.
On the Kotlin
side we’ve been using clikt
to perform the role of swift-argument-parser
but set up is very similar.
Misfires
I spent far too long trying to use cute tricks like #!/usr/bin/env kotlin
with kts
files to get the “scripting” feel with the language of choice.
I personally found this a complete train wreck as I had to pull in loads of dependencies using @file:DependsOn
and then very quickly hit the fact that I can’t write Kotlin
without an IDE.
For some reason IntelliJ was giving me no help with limited syntax highlighting and no suggestions.
To actually get anything working I had to create a project, import dependencies in the normal way and then once I had code that worked and had all the right imports etc I could copy/pasta it over.
At which point I sat scratching my head wondering why my brain hadn’t stopped me doing such a ridiculous thing - e.g. if I only committed the kts
file I’d be committing the lossy version of my work that is hard to debug or work with.