Back

Task automation in Swift with Sake

In the past are those days when you automated your tasks using Make, Rake or Fastlane. These tools provided an interface and tools to automate certain tasks in our projects. Each of them had their strenghs and weaknesses. If we take Make, for instance, it requires some bash knowledge, but on the other side, it is more robust than the other solution (it doesn’t depend on anything that is not in your system). Rake or Fastlane made the definition of our tasks easier using a more readable language like Ruby, but they required to install some dependencies. Fastlane, for example, comes with some dependencies that need to be installed in the system using Ruby gems.

While those tools made our lives easier as developers, I felt we could leverage Swift and its types to automate tasks, using Xcode and its editor to build and test our tasks easily. I started to work on the Swift version of Make that I’m pleased to introduce in this post, Sake. It’s in a very early stage but ready to be used. I’m eager to get feedback from you and improve the tool with your help. Please don’t hesitate to contribute to the repo or propose ideas!

Why Sake? ❓

I’m a Swift/Objective-C developer, and I’m very used to work with compilers that catch any issues with the types in our code. Types are a potent tool and used correctly can make our apps safer bringing a lot of confidence to the developer. In most of the projects that I’ve been involved, either open or closed source, the automation was mainly done using bash or Ruby and tools like Rake or Fastlane. Many times I’ve been myself trying to debug failing lanes or tasks using pry in Ruby or printing stuff in the console to analyze the execution of the task. Sometimes these issues happened on CI and I spent a lot of time debugging them until I could figure out where the issue was coming from. Does it sound familiar to you?

I read about how Swift Package Manager works and saw that Danger did something similar. After some grooming, I realized it was feasible to build a Swift Make where the definition of the tasks would be in a Swift file. What I liked about the idea is the fact that developers would be able to write the scripts in a language they are familiar with. They don’t need to know about the Ruby syntax, or how to debug Ruby execution. Moreover, they could use an IDE like Xcode that they are very used to. The tool would facilitate testing and composition by allowing developers to separate their tasks into multiple .swift files that are part of the same module:

// Sakefile
import SakefileDescription
import SakefileUtils

enum Task: String, CustomStringConvertible {
  case build
  var description: String {
    switch self {
      case .build:
        return "Builds the project"
    }
  }
}

Sake<Task> {
  $0.task(.build) { (utils) in
    // Here is where you define your build task
  }
}.run()

How it works 👩🏻‍💻

Sake is based on the same Swift Package Manager foundation but with some subtle differences. In case of Swift Package Manager, the tool compiles your Package.swift file exporting the variable as a TOML file (a format similar to JSON yet more human-readable). Afterward, they parse that TOML file and map it into models that define the structure of your project.

If you are interested in reading more about that, I’d recommend you to check out this blog post that explains it further.

In case of Sake it doesn’t need to generate any toml file since the tasks are not value properties but closures that contain the actions. Rather, Sake compiles the file passing some arguments that are used by the Sake class to determine what needs to be done (e.g. list all the tasks, or run a particular task). This class contains all the logic that compiles and runs the Sakefile.swift file.

Notice that we need to pass some flags that specify where the libraries are. This crucial if we don’t want the compiler to throw an error when you try to compile the Sakefile.swift because it doesn’t know where to import the SakefileDescription library from.

How to use it 🤔

Install the tool 📦

First of all, you need to install the tool. You can easily do it with Homebrew:

brew tap xcodeswift/sake [email protected]:xcodeswift/sake.git
brew install sake

Notice that you have to add the repository tap. A tap in Homebrew is a repository that contains formulas, and formulas are Ruby classes that specify how to install a given tool. I plan to add the formula to the official Homebrew tap so that you don’t have to add any external tap, but it’s pending task to do.

Generate your Sakefile.swift 📝

Once you have the tool installed you should be able to run the following command from the console:

sake init

sake init will generate a base Sakefile.swift in the current directory where you can start defining your tasks.

Notice that Sake is a generic class where the type must be an enum that contains all possible tasks. That makes the definition of your tasks more type-safe since you can define dependencies between tasks by passing the type rather than a string with its name.

Generate the Xcode project ⚙

You can edit the file above using any text editor. That’s a good thing to do if you want to practice how familiar you are with the Swift syntax and Foundation APIs but is that something that you’d like to practice? What if we could use Xcode and benefit from the syntax highlighting and the code autocompletion that the IDE offers? What if I told you that Sake could help you with that?

Run the following command on the console:

sake generate-xcodeproj

The command will generate a Sakefile.xcpdeproj project in the directory where the Sakefile.swift is. The project contains a command-line-tool target with your Sakefile.swift and the linking settings necessary for Xcode to recognize the tool libraries.

:warning: Xcode expects command-line-tool targets to contain a main.swift file that is the entry point of the command line tool. That’s an implicit value that we can’t change. If you try to compile the scheme you’ll see that Xcode complains because you cannot run code at the top level in files other than the main.swift file. Ignore the error since it will work when the Sakefile.swift is run by Sake.

Running tasks ✅

Running tasks is very easy. You just need to run the following command where name is the name of the task:

sake task name

That would run the task erroring if the task failed (by throwing a Swift error). Sake also provides a command that you can use to print all the available tasks:

sake tasks

One more thing… 🍎

Hooks

Sake supports hooks that allow you to define code that will be executed before or after of each or all the tasks:

// Sakefile
Sake<Task> {
  $0.beforeEach {
    // Do something
  }
  $0.afterAll {
    // Do something
  }
}.run()

Utils

When tasks or hooks are created the closure takes an object that contains some utils that are provided by Sake. The utils that are currently provided are:

In the example below you can see how the utils are used to automate the release process of Sake:

let branch = "release/1.0.0"
try utils.git.createBranch(branch)
try utils.shell.runAndPrint(bash: "swift build")
try utils.git.addAll()
try utils.git.commitAll(message: "[\(branch)] Bump version")
try utils.git.tag(version)
try utils.git.push(remote: "origin", branch: branch, tags: true)

Limitations 😅

Although the tool is entirely functional some important limitations are worth considering:

Upcoming features 🤗

Sake is in a very early stage and I’m eager to get feedback from you to improve the tool. Some ideas that are in the backlog are:


I hope you liked this introductory blog post. Can’t wait to see how you use the tool and the ideas you come up with. Sake is part of xcode.swift, an open source organization on GitHub that aims to build open source tools written in Swift to facilitate developers work when working with Xcode. If you are interested in contributing don’t hesitate to do it, there’s a Slack channel that you are invited to join and talk to other contributors.