How to Use Swift Packages in Bazel Using swift_bazel

Swift Package Manager and Bazel

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 swift_bazel project consists of two parts: a Gazelle extension and a set of Bazel repository rules (swift_package, local_swift_package).

The Gazelle extension does the following:

  1. Inspects files in your source tree (e.g., Package.swift),
  2. Resolves the transitive list of external Swift packages,
  3. Writes a swift_package declaration for each external Swift package, and
  4. Optionally, generates swift_xxx declarations for your project.

During the Bazel build, the swift_package repository rule performs the following actions for each external Swift package:

  1. Downloads the Swift package, and
  2. 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.

Simple Example

Let’s walk through a simple example adding apple/swift-log to a Bazel workspace.

1. Configure your workspace to use swift_bazel.

Update the WORKSPACE file to load the dependencies for swift_bazel, rules_swift and Gazelle.

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()

The above 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.

The 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_package declarations. 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, swift_package and 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. .gitignore).

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 swiftpkg_<identity>. The <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 swift_deps.bzl file.

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 Foo with 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 @swiftpkg_cool_pkg//:printhello.
  • 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_bazel does 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:

  1. Scours your workspace looking for Swift source files.
  2. Identifies your Swift libraries, binaries and tests based upon the shape of your project directories.
  3. Parses the Swift source files for import statements.
  4. Identifies the Bazel target(s) that provide the imported modules.
  5. Generates swift_xxx declarations 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:

  • Package.resolved
  • swift_deps.bzl
  • swift_deps_index.json
  • The Bazel build files that we generated or updated

Conclusion

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:

If you have questions, head over to our discussions area or the apple channel in the Bazel Slack. If you found a bug or would like to request a feature, file an issue.