Replies: 3 comments 1 reply
-
Thanks for writing this up @cgrindel ! To be clear, my goal is to remove the swift_deps_index.json from the repository, but I don't mind if an index like this exists inside of the repository rule(s). Also hopefully we could find a way to keep the .build directory out of the repository as well. In my ideal world only Package.swift and Package.resolved would be checked into the repository (with the latter being an optional lock file, similar to the optional SHAs for the http_file / http_archive rules.) I also understand the complexities around swift package resolution and the product / target dilemma make things more difficult. Firstly, while
I believe this JSON file contains most of what we need. Its format is:
Note that the dependencies here contain not only direct dependencies but transitive dependencies as well. The one thing that this file is missing is the dependencies between packages and targets / products within those targets. However, with the checkouts, this is easily solved. We can have an action change directory into the To avoid re-running To avoid checking the .build directory into the repository, I'm thinking we could could copy / symlink the Package.swift (and potentially Package.resolved) files into a temporary directory created by an action of the the root repository rule and run the swift package commands inside that directory. The workspace-state.json file could be declared to Bazel and the Package.swift (+Package.resolved) would be file target attributes so it would be hermetic and cache-able. The action would build the full index (potentially with the same format as the existing swift-deps-index.json file) which would be consumed by the other repository rules. To summarize, my proposal is as follows:
|
Beta Was this translation helpful? Give feedback.
-
Following up here, I created an example repository demonstrating how my proposed approach can work with a single module extension, no .build in the repository, and no additional files checked into the repository: |
Beta Was this translation helpful? Give feedback.
-
This has been done in recent releases. |
Beta Was this translation helpful? Give feedback.
-
Some members of the community have expressed a desire to reduce or eliminate the index JSON file that is currently checked into repositories that use
rules_swift_package_manager
. The following is an overview of the information that is currently stored in the file and an explanation of why it was created.Structure
direct_dep_identities
: This is a list of the Swift packages that are direct dependencies of the client's workspace.modules
: This is a list of all the Swift modules declared by the direct and transitive dependencies of the client's workspace. The important pieces of information are the name, the type, and the Bazel target label.products
: This is a list of all the Swift products declared by the direct and transitive dependencies of the client's workspace. The important pieces of information are the name, the type and the Swift module targets that are required to depend on the product.packages
: This is a list of all of the Swift packages that are required by the direct and transitive dependencies of the client's workspace. This tellsrules_swift_package_manager
how to construct theswift_package
declarations.How is it generated?
It is generated by running the
//:swift_update_pkgs
target. Behind the scenes, this is a call toGazelle's
update-repos
command. This command fully resolves the transitive dependencies using SPMcommands (e.g.
swift package resolve
). This, in turn, downloads all of the transitive dependenciesresulting in the annoying
.build
directory in the workspace.How is the index JSON file used?
The index JSON file is used to:
swift_package
) generated by the bzlmod logic.swift_package
).Why do we need an index file when Go and other languages do not need one?
This has to do with how imports work in different languages. In short, Swift imports are not unique
and they do not map one-to-one to a Bazel target.
Background
Let's look at Go imports. They are unique. Outside of the standard packages, the imports are string
values derived from a package URL (e.g.
github.com/stretchr/testify/assert
). It is easy todifferentiate between two different but similarly named packages (e.g.
github.com/stretchr/testify/assert
vsgotest.tools/v3/assert
). Also, Go packages convenientlymap to a single Bazel target via
go_library
declarations. Hence, the Go Gazelle plugin can readthe import string value and know exactly which Bazel target should be associated.
Unfortunately, Swift is not nearly as convenient. First, Swift imports are non-unique string values
(e.g.
Logging
). The value alone does not tell us which Swift package provides the module. To fullyresolve to the correct module, we need information from the package manifest. Second and more
insidiously, a Swift module import can result in one or more Bazel targets needing to be depended
upon to compile properly. This is mostly an issue with Swift products that include multiple Swift
targets. Notice that the product declaration can accept multiple Swift targets.
Construct the repository rule declarations (e.g.
swift_package
) generated by the bzlmod logic.To generate the
swift_package
declarations, we need the remote and commit information for all ofthe Swift packages (direct and transitive) and we need information to resolve Swift target
dependencies (see next section). The remote and commit information exists in the
Package.resolved
.Unfortunately, the information to properly resolve the Swift target dependencies is not present in
the
Package.resolved
.Generate Bazel build files in the repository rules (e.g.
swift_package
).In SPM, Swift targets can depend on Swift targets or Swift products. As mentioned previously, Swift
products can consist of one or more Swift targets. So, Swift products can map to one or more Bazel
targets (Swift target = Bazel target). There is no construct in Bazel that represents a group of
targets. So, to properly resolve a dependency on a Swift product, we need a mapping of products to
Bazel targets.
NOTE: Without fully resolving the transitive dependencies for a package, there is no way to know
what targets are associated with a Swift product.
Generate Bazel build files in the client's workspace via the Gazelle plugin.
To properly map Swift import statements to Bazel targets, we need to know
similarly named modules in the transitive dependencies.)
Short of parsing and evaluating the
Package.swift
, there is no way to know which of the Swiftpackages are direct dependencies for the client workspace. Hence, we store this in the Swift index
JSON file.
Could we eliminate the Swift index file?
Potentially. However, there are some trade-offs.
Construct the repository rule declarations (e.g.
swift_package
) generated by the bzlmod logic.The information for downloading the Swift packages is in the
Package.resolved
. If we are gettingrid of the Swift index file, then there is no need to pass that along. So, we should be all set.
Generate Bazel build files in the repository rules (e.g.
swift_package
).Repository rules execute independently. There is no way to tell Bazel that one repository rule
depends on another repository rule. So, to generate build files, the repository rules must have all
of the information that they need or be able to derive it. If we do not provide the Swift index
file, each Swift package must fully resolve their own transitive dependencies.
As was mentioned previously, this is done using SPM commands. So, each Swift package would need to
do the following:
swift package update
. This downloads the transitive dependencies forthe Swift package.
Let's look at an example project that depends on two Swift packages:
A
andB
. PackageB
depends on two other Swift packages:
C
andD
.To generate build files for
A
,B
,C
, andD
, we would need toA
once (repository rule).B
once (repository rule).C
two times (repository rule,B
resolution)D
three times (repository rule,B
resolution,C
resolution)In this example with four Swift packages, we would have performed seven downloads and generated five
maps. As you can see, deeper dependency trees result in costlier, duplicative work.
Generate Bazel build files in the client's workspace via the Gazelle plugin.
To generate build files for the client, we need to know the Swift packages that the client depends
upon and we need to generate a map of the Swift modules to Bazel targets. The list of direct
dependencies can be derived by running
swift package dump
on the client workspace and the Swiftpackages could provide a file that maps the Swift modules/targets to Bazel targets.
Conclusion
Ultimately, I opted to include the Swift index JSON file to solve a couple of needs. However, the
primary win is that we cache information about the dependencies. This avoids extra work when
performing builds and reduces some complexity when debugging resolution issues.
Beta Was this translation helpful? Give feedback.
All reactions