Hi All,
While this thread is a few years old, if we're looking at revisiting SwiftPM's package resolution I think it might be worth looking at how Go's resolution system works. It has some desirable attributes, such as being reproducible without conflicts between a manifest and a lock file. Please note that I'm using SwiftPM terminology where the Go terminology is a bit different below so that the parallels can be drawn. The links below will of course use the original Go terms.
How does it accomplish this? First, it uses a minimum, instead of a maximum version selection algorithm for resolution. Given a set of versions in a manifests in the transitive closure of packages the resolution will always be the same outside of some kind of tampering or rewriting of history. Minimum version selection also has a side effect of promoting dependency cooldowns, which can help to mitigate common types of supply chain attacks.
So, how does the Go package manager help to avoid tampering? There is both a public sum database, as well as a local sum file with checksums for the currently selected versions. If there's tampering then the package manager produces loud errors about it. This file can be committed, which is recommended, or left uncommitted and the package resolution remains repeatable without the extra level of checksums.
If SwiftPM were to adopt a similar mechanism, then it might gain the overall benefits of conflict freedom and reproducibility. There are additional capabilities in SwiftPM that would need to be considered in this mechanism. The first is the branch-based dependency.
Branch-based dependencies are inherently maximum version selecting from a package manifest perspective, which requires some additional piece of metadata to indicate what revision is resolved at a given time to prevent each build from attempting to fetch and checkout the latest from the package's branch. This would yield an unsatisfactory user experience, and so today it is the Package.resolved that indicates the last commit that was resolved, awaiting the user to do a swift package update to bring it to the latest commit at that point. Committing the resolved commit in the Package.resolved might help to make things more reproducible at the cost of eliminating the maximum version selection. Leaving the file uncommitted limits reproducibility for all packages permitting resolution to be closer to maximum version selection of the branch in CI, or a clean workspace.
Go can handle branch-based dependencies with a separate package replacement mechanism where a package can be given an alternate local path. Branch-based dependencies are often used in place of a git mono-repository, and so this replacement gives the opportunity for an external entity (or user) to manually align the revisions. If SwiftPM had a similar replacement mechanism then the package manifest could then use regular repo and version-based constraints, and the replacement list can be used to override it in the workspace.
In summary, we can have reproducible SwiftPM builds without a lock file that can potentially conflict with the package manifest by transforming the lock file into a simple sum database with the purpose of helping to detect tampering. This is accomplished using minimum version selection, which has another nice advantage relating to supply chain attack mitigation. The case of branch-based dependencies and their inherent maximal version selection can be handled in another way with a package replacement mechanism that allows external entities to align the versions independently.
Cheers,
Chris