How to Use Swift Packages in Bazel Using
If you build Swift software in Bazel, you probably use the excellent rules_swift rules to develop modules and executables for your project. While working on your project, you almost certainly have wanted to import one of the many third-party Swift packages that exist. Unfortunately, rules_swift does not provide a means to integrate these packages into your project quickly. So, I created swift_bazel to address this very issue. Please read on to learn how it use Swift packages in Bazel using swift_bazel.
NOTE: I have written previous posts on how to use rules_spm to leverage external Swift packages in your Bazel workspace. Due to some shortcomings in the design of rules_spm, it became apparent that a new way forward was necessary. The swift_bazel project is that new way forward.
Table of Contents
What is swift_bazel?
The Gazelle extension does the following:
- Inspects files in your source tree (e.g.,
- Resolves the transitive list of external Swift packages,
- Writes a
swift_packagedeclaration for each external Swift package, and
- Optionally, generates
swift_xxxdeclarations for your project.
During the Bazel build, the
swift_package repository rule performs the following actions for each external Swift package:
- Downloads the Swift package, and
- Generates a Bazel build file for the Swift package.
What is a Gazelle extension?
Gazelle is a Bazel build file generator. It started out as a tool to generate Bazel build files for Go source files. However, the maintainers added a framework for extending it to support other languages. The list of supported languages is growing quickly.
Let’s walk through a simple example adding apple/swift-log to a Bazel workspace.
1. Configure your workspace to use swift_bazel.
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( name = "cgrindel_swift_bazel", sha256 = "de685bdb06ffb4ddb558d810d56d99a6e0fed44bd770a422e41dcea4fc3f6c2d", strip_prefix = "swift_bazel-0.1.0", urls = [ "http://github.com/cgrindel/swift_bazel/archive/v0.1.0.tar.gz", ], ) load("@cgrindel_swift_bazel//:deps.bzl", "swift_bazel_dependencies") swift_bazel_dependencies() load("@cgrindel_bazel_starlib//:deps.bzl", "bazel_starlib_dependencies") bazel_starlib_dependencies() # MARK: - Gazelle # gazelle:repo bazel_gazelle load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") load("@cgrindel_swift_bazel//:go_deps.bzl", "swift_bazel_go_dependencies") load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") # Declare Go dependencies before calling go_rules_dependencies. swift_bazel_go_dependencies() go_rules_dependencies() go_register_toolchains(version = "1.19.1") gazelle_dependencies() # MARK: - Swift Toolchain http_archive( name = "build_bazel_rules_swift", sha256 = "32f95dbe6a88eb298aaa790f05065434f32a662c65ec0a6aabdaf6881e4f169f", url = "https://github.com/bazelbuild/rules_swift/releases/download/1.5.0/rules_swift.1.5.0.tar.gz", ) load( "@build_bazel_rules_swift//swift:repositories.bzl", "swift_rules_dependencies", ) load("//:swift_deps.bzl", "swift_dependencies") # gazelle:repository_macro swift_deps.bzl%swift_dependencies swift_dependencies() swift_rules_dependencies() load( "@build_bazel_rules_swift//swift:extras.bzl", "swift_rules_extra_dependencies", ) swift_rules_extra_dependencies()
WORKSPACE boilerplate loads a file called
swift_deps.bzl. The Gazelle extension will populate it, shortly. For now, create the file with the follwing contents:
# Contents of swift_deps.bzl def swift_dependencies(): pass
2. Create a minimal
Package.swift file is the source of truth for your external Swift packages. This file contains the list of packages that are directly used by your code.
We only need to list the external dependencies in the
Package.swift file. There is no need to fully describe your build in the
Package.swift. For our example, populate the file with the following:
// swift-tools-version: 5.7 import PackageDescription let package = Package( name: "my-project", dependencies: [ .package(url: "https://github.com/apple/swift-log", from: "1.4.4"), ] )
The name of the package can be whatever you like. It is required for the manifest, but it is not used by swift_bazel.
3. Add Gazelle targets to
BUILD.bazel at the root of your workspace.
Next, we need to add some targets to our Bazel workspace that will do the magic. Add the following to the
BUILD.bazel file at the root of your workspace.
load("@bazel_gazelle//:def.bzl", "gazelle", "gazelle_binary") # Ignore the `.build` folder that is created by running Swift package manager # commands. The Swift Gazelle extension executes some Swift package manager commands to resolve # external dependencies. This results in a `.build` file being created. # NOTE: Swift package manager is not used to build any of the external packages. The `.build` # directory should be ignored. Be sure to configure your source control to ignore it (i.e., add it # to your `.gitignore`). # gazelle:exclude .build # This declaration builds a Gazelle binary that incorporates all of the Gazelle extensions for the # languages that you use in your workspace. In this example, we are only using the Gazelle extension # from `swift_bazel`. If you are using any other Gazelle extensions, list them here. gazelle_binary( name = "gazelle_bin", languages = [ "@cgrindel_swift_bazel//gazelle", ], ) # This target should be run whenever the list of external dependencies is updated in the # `Package.swift`. Running this target will populate the `swift_deps.bzl` with `swift_package` # declarations for all of the direct and transitive Swift packages that your project uses. gazelle( name = "swift_update_repos", args = [ "-from_file=Package.swift", "-to_macro=swift_deps.bzl%swift_dependencies", "-prune", ], command = "update-repos", gazelle = ":gazelle_bin", ) # This is optional. When executed, this target updates the Bazel build files for your project for # the languages listed in the `gazelle_bin` declaration. Run this target whenever you make changes # to your source files that need to be reflected in the Bazel build files (e.g., add/remove source # files, add/remove imports). gazelle( name = "update_build_files", gazelle = ":gazelle_bin", )
4. Resolve the external dependencies for your project.
Now, we are ready to resolve the external dependencies and generate the files that will be needed to build those dependencies in Bazel. Execute the following at the command line:
$ bazel run //:swift_update_repos
This generates several files and directories:
Package.resolved: Swift package manager records the results of the dependency resolution in this file. The exact versions for the selected dependencies are stored in this file.
swift_deps.bzl: This file contains the
swift_packagedeclarations. This file configures Bazel to download and build the external Swift packages.
swift_deps_index.json: This file is used by the Gazelle extension and the repository rules,
local_swift_package. It lists all of the modules and products that are available in the external Swift packages.
.build: This directory is created by Swift package manager. It is not directly used by the Gazelle extension or the repository rules. It is highly recommended to ignore this directory in your source control config (e.g.
At this point, you are ready to start referencing the external Swift packages in your Bazel build files.
How do I reference the Bazel targets for the Swift packages?
The external Swift package repository names follow the pattern
<identity> is the Swift package manager identity value with any hyphen characters (
-) converted to underscore characters (
_). To see the list of repository names be sure to peruse the generated
What Bazel targets are defined?
To find out what targets are available under a package, use
bazel query. For instance, if we have a Swift package called
cool-pkg, you will run the following to see a list of the Bazel targets available for the Swifit package.
$ bazel query @swiftpkg_cool_pkg//:all
Here is a summary of the Bazel targets that are defined for each Swift package:
- Each Swift target is represented by a single Bazel target. For instance, if a Swift package defines a Swift target called
Foowith its sources under
Sources/Foo, the Bazel target will be
@swiftpkg_cool_pkg//:Sources/Foo. Note the placement of the colon (
:). The target is defined at the root of the package repository’s workspace.
- Each Swift executable product is represented by a single Bazel target. If a Swift package defines a Swift executable product named
printhello, the Bazel target will be
- Swift library products are a special case. A Swift library product is really a shorthand for depending upon one or more library targets. There is no equivalent for this in Bazel. Instead, we just depend upon the Bazel target that provides the desired module. However, instead of just ignoring library products,
swift_bazeldoes generate a build_test target with all of the library product’s targets listed. This allows you to test that an exported product builds properly. For example, if a Swift package defines a library product called
MyLibraryProduct, swift_bazel generates a build_test target called
MyLibraryProductBuildTest. You can execute the test by running the following:
$ bazel test @swiftpkg_cool_pkg//:MyLibraryProductBuildTest
Now, you can stop here and start using the external Swift packages in your workspace. However, it will be up to you to manually update your Bazel build files with the correct references. Alternatively, you can read on to find out how swift_bazel can do it for you.
5. Create or update Bazel build files for your project.
So, here is the game-changing part. We can have Gazelle create/update your Bazel build files based upon the Swift source files in our project. Run the following to have Gazelle update our Bazel build files:
$ bazel run //:update_build_files
This command does the following:
- Scours your workspace looking for Swift source files.
- Identifies your Swift libraries, binaries and tests based upon the shape of your project directories.
- Parses the Swift source files for
- Identifies the Bazel target(s) that provide the imported modules.
swift_xxxdeclarations listing your sources and the dependencies that they require to build.
6. Build and test your project.
Now, we are ready to build and test our project.
$ bazel test //...
7. Check in the generated files.
Before we declare victory and celebrate our success, there is one last thing to do. Check in the following generated files:
- The Bazel build files that we generated or updated
In this article, we reviewed how we can use swift_bazel to leverage external Swift packages in a Bazel workspace. To learn more, check out the following: