How to Integrate SwiftFormat into 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
- You are doing Swift development using Bazel and rules_swift;
- You have decided to use nicklockwood/SwiftFormat as the code formatter for your Swift code;
- Your team has agreed upon a coding style;
- You have created a SwiftFormat configuration file that will apply the team’s coding style.
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.