Project Scripts

TL;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 curling 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.