How to Integrate SwiftFormat into Bazel

SwiftFormat and Bazel

This article introduces rules_swiftformat, a set of Bazel rules and macros that format Swift source files using nicklockwood/SwiftFormat, test that the formatted files exist in the workspace directory, and copy the formatted files to the workspace directory.

Table of Contents

Good Code Hygiene

In a previous article, I wrote about the benefits of teams adopting a standard coding style. The following quote nicely summarizes the argument:

Consistently using the same style throughout your code makes it easier to read. Code that is easy to read is easier to understand by you as well as by potential collaborators. Therefore, adhering to a coding style reduces the risk of mistakes and makes it easier to work together on software.

Automated tools like code linters and code formatters can aid the adoption and maintenance of team coding styles. When integrated into a developer’s workflow and a team’s continuous integration system, these tools can maintain good code hygiene.

Formatting Swift Code in Bazel

For this article, we are going to assume that

Now, the only thing missing is how to integrate SwiftFormat along with your team’s configuration file into your workflow.

SwiftFormat Integration Options

The developers on your team will want to configure their IDE to format Swift source files that they are editing. The SwiftFormat repository has an extensive list of installation and integration options.

While IDE integration will handle most of the formatting needs for your team, it does not ensure that all of the Swift source files are formatted properly. Automated code changes (i.e., batch source changes using sed scripts) or slow/faulty IDE integration can allow improperly formatted source files to be committed in source control. So, to help with your team’s code hygiene, you will want to add a check to ensure that only correctly formatted source files make it to your main branch.

One option for applying this check is to add a step to your continuous integration (CI) workflow, which executes SwiftFormat as a linter. If violations exist, SwiftFormat will fail. This scheme works well. Unfortunately, the feedback loop for the developer is pretty long with this type of setup. A developer may not discover that something is improperly formatted until they submit a pull request (PR) for review.

Enter rules_swiftformat

The rules_swiftformat Bazel rules integrate Swift source file formatting directly into the normal build-test cycle that developers use everyday. The repository contains rules and macros that format Swift source files using nicklockwood/SwiftFormat, test that the formatted files exist in the workspace directory, and copy the formatted files to the workspace directory. With this scheme, developers will know that all is well with their code long before they attempt to push their code for review.

Implementing rules_swiftformat for your Bazel project

The following provides a quick introduction on how to use the rules in this repository. For more information, check out the documentation and the examples.

1. Configure your workspace to use rules_swiftformat

Add the following to your WORKSPACE file. It will download the dependencies for rules_swiftformat and configure them for use.


# Download and configure rules_swiftformat.

http_archive(
    name = "cgrindel_rules_swiftformat",
    sha256 = "4942ca4f8f88d926964c7ff9c449c5c7eb2e0f1059675d16f75ea57bfdebb504",
    strip_prefix = "rules_swiftformat-0.1.0",
    urls = ["https://github.com/cgrindel/rules_swiftformat/archive/v0.1.0.tar.gz"],
)

load("@cgrindel_rules_swiftformat//swiftformat:deps.bzl", "swiftformat_rules_dependencies")

swiftformat_rules_dependencies()

# Configure the dependencies for rules_swiftformat

load(
    "@cgrindel_rules_spm//spm:deps.bzl",
    "spm_rules_dependencies",
)

spm_rules_dependencies()

load(
    "@build_bazel_rules_swift//swift:repositories.bzl",
    "swift_rules_dependencies",
)

swift_rules_dependencies()

load(
    "@build_bazel_rules_swift//swift:extras.bzl",
    "swift_rules_extra_dependencies",
)

swift_rules_extra_dependencies()

# We are using rules_spm to download and build SwiftFormat. The following will configure
# rules_spm to do that.

load("@cgrindel_rules_swiftformat//swiftformat:load_package.bzl", "swiftformat_load_package")

swiftformat_load_package()

2. Update the BUILD.bazel at the root of your workspace

At the root of your workspace, create a BUILD.bazel file if you don’t have one. Add the following:

load(
    "@cgrindel_rules_swiftformat//swiftformat:swiftformat.bzl",
    "swiftformat_update_all",
)

# This will export your team's SwiftFormat configuration file, making it 
# available to other Bazel packages in the workspace.
exports_files([".swiftformat"])

# Define a runnable target to copy all of the formatted files to the workspace directory.
swiftformat_update_all(
    name = "update_all",
)

The exports_files declaration defines a target for your SwiftFormat configuration file (.swiftformat). The swiftformat_pkg declarations that we will add to each of the Bazel packages in a follow-on step reference this target automatically. If your SwiftFormat configuration file has a different name or is located in a different directory, you will need to specify the appropriate label to the config attribute for swiftformat_pkg or the swiftformat_config attribute for swiftformat_XXX.

The swiftformat_update_all macro defines a runnable target that copies all the formatted Swift source files to the workspace directory.

3. Add swiftformat_pkg to every Bazel package with Swift source files

NOTE: Don’t do this step until you have read the next section. It could save you some time and avoid a few headaches.

In every Bazel package that contains Swift source files, add a swiftformat_pkg declaration.

load(
    "@cgrindel_rules_swiftformat//swiftformat:swiftformat.bzl",
    "swiftformat_pkg",
)

swiftformat_pkg(
    name = "swiftformat",
)

The swiftformat_pkg macro defines targets for a Bazel package which will format the Swift source files, test that the formatted files are in the workspace directory, and copy the formatted files to the workspace directory.

4. Format, Test, and Update

With a few simple commands, you can format the Swift source files, execute the tests that ensure the formatted sources are in the workspace directory and copy the formatted source files back to the workspace.

# Build, format, and test your Swift sources
$ bazel test //...

# If one or more of the format checks fails from the previous command, you can
# copy the formatted files to the workspace directory.
$ bazel run //:update_all

Integrate with rules_swift

Hopefully, the idea of adding source formatting to your build sounds enticing. However, there are two issues with the previously described recipe. First, there is no easy way to ensure that newly added Bazel packages include the swiftformat_pkg declaration. Second, for a mature codebase, you may have a large number of Bazel packages to touch. So, let’s look at a way to roll out rules_swiftformat, making it easier to implement and maintain.

Option #1: Use macros provided by rules_swiftformat

The rules_swiftformat project comes with three macros that combine their rules_swift counterpart with swiftformat_pkg. They are called swiftformat_library, swiftformat_binary, and swiftformat_test. To use them as drop-in replacements, change the load statements in your Bazel build files from load("@build_bazel_rules_swift//swift:swift.bzl", "swift_XXX") to load("@cgrindel_rules_swiftformat//swiftformat:swiftformat.bzl", swift_XXX = "swiftformat_XXX") where XXX is library, binary, or test.

This works great. However, you may want to incorporate other Swift-related declarations in the future. So, let’s go ahead and create macros that use the ones provided by rules_swiftformat.

Option #2: Create custom macros for swift_library, swift_binary, and swift_test

Let’s create Bazel macros for swift_library, swift_binary, and swift_test that will combine the rules_swift functionality with the swiftformat_pkg formatting capabilities. Then, we merely need to update the load statements in your Bazel build files to use the new macros.

NOTE: To see a working example of this technique, check out our Swift Toolbox repository.

1. Create a Bazel package for the custom macros.

Create the build/swift directories at the root of your workspace and add a BUILD.bazel file.

$ mkdir -p build/swift
$ echo "# Intentionally blank" >> build/swift/BUILD.bazel

2. Create custom macros

Now, create a swift_library.bzl file with the following contents:

load(
    "@cgrindel_rules_swiftformat//swiftformat:swiftformat.bzl",
    "swiftformat_library",
)

def swift_library(name, srcs = None, **kwargs):
    # This conditional statement is an example of custom logic that you can add.
    if srcs == None:
        srcs = native.glob(["*.swift"])

    swiftformat_library(
        name = name,
        srcs = srcs,
        **kwargs
    )

Next, create a swift_test.bzl file and a swift_binary.bzl file using the same template as above. Be sure to change _library to the appropriate suffix.

3. Update the load statements

Lastly, update the load statements for all of the Bazel build files that contain swift_XXX declarations so that they load your macro instead of the original rule. This can be done easily using Buildozer. For example, to replace the load statements for swift_library, you can do something like the following:

$ buildozer 'replace_load //build/swift:swift_library.bzl swift_library' //Sources/...:__pkg__

The resulting build files should look something like the following:

load("//build/swift:swift_library.bzl", "swift_library")

swift_library(
    name = "DateUtils",
    module_name = "DateUtils",
    visibility = ["//visibility:public"],
)

4. Format, Build, Test and Update

As mentioned before, you can format, build, test and update your code using the following commands:

# Build, format, and test your Swift sources
$ bazel test //...

# If one or more of the format checks fails from the previous command, you can
# copy the formatted files to the workspace directory.
$ bazel run //:update_all

Conclusion

This article demonstrated how to use rules_swiftformat to integrate nicklockwood/SwiftFormat into your Bazel projects. Hopefully, it will help your projects maintain good code hygiene.