I finished up my last bug fix and tackled another. SR-4261 took me deep into some new areas of the code base that I wasn’t familiar with. I did more spelunking to have a decent clue of what the code was doing before making the fix. The results of the spelunking are this post.
Many of the recent changes to SwiftPM have been about managing the workspace of a project – pinning dependencies, updating and editing dependencies, and handling that whole space of interactions. These are exposed
as commands under
swift package, such as
swift package edit and
swift package pin.
In the codebase, most of the high level logic for these commands resides in the target Workspace.
If you want a map of all the targets in the project, I included a graph of targets in my first round of spelunking swift package manager.
There are a number of interesting classes in Workspace, but the ones I focused on
were related to manifests and managed dependencies: Manifest and ManagedDependency. These two are combined in Workspace in DependencyManifest which represents SwiftPM’s knowledge of the workspace, its current state, and provides the means to manipulate the workspace with commands like
When swift package manager wants to manipulate the workspace, the logic
starts out loading the current state. This pretty consistently starts with loading
up the main targets by invoking
loadRootManifests. This in turn uses the manifest loading logic and an interesting (newish mechanism to SwiftPM) piece: a DiagnosticsEngine. The DiagnosticsEngine collects errors and can emit interesting details for tooling wanting to provide more UI or feedback information.
loadRootManifests loads this from a list of AbsolutePath that gets passed in from the swift CLI – generally the current working directory of your project. In any case, loadRootManifests returns an array of Manifest, which is the key to loading up information about the rest of the workspace.
The next step is often
loadDependencyManifests that returns an instance of DependencyManifests. This does the work of loading the dependencies needed to create the holistic view of the project to date and load the relevant state. Loading the ManagedDependencies leverages the class LoadableResult. LoadableResult is a generic class for loading persistence from a JSON file – Pins and ManagedDependencies are both loaded using it. In the case of ManagedDependencies, it loads from the file
dependencies-state.json, which includes loading up the current state, current path, and relevant details about the repository. That file, along with validating the relevant repository exists (using
validateEditedPackages) at the correct location on disk, also indicates any packages in the “edit” state.
Pins work a bit differently, being stored in the source path. This is to allow them to be included in the project source in order to concretely specify versions or constraints to versions for each dependency. The ManagedDependencies are maintained in the
working directory for builds, not expected to be in source control, and represent the state of things on your local machine.
The way that SwiftPM handles dependency resolution is by using a collection of
RepositoryPackageConstraint and a constraint solver to
resolveDependencies. The DependencyResolver is it’s own separate thing under the
PackageGraph package. I have not yet dug into it. Most notably, The DependencyResolver will throw an error if it’s unable to resolve the constraints provided to it – and that’s key to the heart of the bug SR-4261, which is about adding in missing constraints for edited packages when invoking the pin command.