The following is a draft proposal, feedback welcome.

____________
SwiftPM Dependency Version Locking
Proposal: SE-NNNN 
<https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-swiftpm-dependency-lockfiles.md>
Author(s): Ankit Agarwal <https://github.com/aciidb0mb3r>, Max Howell 
<https://github.com/mxcl>
Status: Discussion
Review manager: Rick Ballard
Introduction
This proposal seeks to declare a new, generated file Packages/VersionLocks.json 
that describes the exact state of a package’s dependency graph and then by 
default will be respected when executing most package manager commands. Thus it 
is considered a “version lock” for a package’s dependency sources.

Swift-evolution thread 
<https://lists.swift.org/pipermail/swift-build-dev/Week-of-Mon-20151214/000067.html>
Terminology
A package refers to a published, versioned git repository designed to be 
consumed as a dependency by SwiftPM.
A project refers to an end-user workspace that uses SwiftPM (via a 
Package.swift and swift build) fetching and building packages as part of its 
build
Describing this distinction is required because both the above have the same 
form, but are used differently by an end-user. An end-user may publish 
packages, but will eventually consume those packages in a project.

As justification for this confusion, it is considered a feature that projects 
can easily and trivially become packages when using SwiftPM. Encouraging a 
vibrant packaging ecosystem is one of our goals.

Motivation
In a vibrant packaging ecosystem, dependencies update continuously with 
bug-fixes and new features. A development team needs:

To ensure they are all using the same versions of their dependencies for any 
given version-control commit.
Ensure they are all using the same versions of the underlying Swift toolchain
Be able to override or modify dependency specifications for the whole team for 
specific commits.
Currently with SwiftPM it is possible to fulfill 1. by committing the sources 
of a package’s dependencies with the package itself, but this is not always 
desirable. There is no way to achieve 2. and 3. with SwiftPM alone.

Additionally, there is not currently a way to know which version of Swift a 
package requires to build. At this time this situation is particularly 
precarious because Swift itself is not backwards compatible. As a Swift 
developer at the very least recording which Swift version a package was built 
with by the package developer is essential information in order to assess a 
package's suitability. Practically the package manager could in the future use 
this information to aid an end-user or even fix the problem when packages fail 
to compile.

Proposed Solution
A file: Packages/VersionLocks.json will be created alongside the Package.swift 
file. Its contents will describe:

The URL and versions of cloned dependencies
An inline diff of any local modifications made to those packages relative to 
their pristine cloned states
The exact version of the Swift toolchain used as part of the last successful 
build of the package
This file is generated by SwiftPM.

This file should be checked-in with projects.

This file is generated and should not be edited by users. If the file is edited 
by users the behavior is undefined.

This file should be checked-in with packages designed for consumption in 
projects, however SwiftPM will not use the checkout files of dependencies when 
determining a project’s dependency graph (this would make dependency graphs 
much less likely to resolve due to overly strict versioning requirements). In 
the future we may choose to make it possible for end-users to attempt to build 
a package using all checkout files since in certain deployment scenarios where 
an exact graph has already been tested, this is a solid reliabiity feature.

Any local, modifications made to the clones in Packages are recorded in 
Packages/VersionLocks.json as part of the flow described in the next section. 
Modifications here means: changes to git remotes and the git-ref of the 
checked-out HEAD.

Detailed Design
In a fresh clone that does not contain a Packages directory swift build will 
determine the dependency graph, clone the packages into Packages and generate a 
Packages/VersionLocks.json file.

The user can now step into the Packages directory and modify package sources. 
If the user then runs swift build again the package manager will error out:

error: dependency sources have been modified
execute `swift build --lock` or `swift build --ignore-lock`
It is an error to build against an unlocked dependency graph, but to facilitate 
fixing bugs etc. an ignore flag can be specified.

When swift build --lock is specified the package manager regenerates the 
lockfile detailing the active git remote and the SHA that is checked-out.

Every time swift build completes a build the lockfile is updated (if necessary) 
recording the current version of the Swift toolchain that achieved the build.

Packages/VersionLocks.json

The exact design of the contents of this file will be explored during iterative 
development, but here is a possible example:

json { "packages": [ { "clone": "Packages/PromiseKit-3.0.3", "origin": 
"https://github.com/mxcl/PromiseKit"; "ref": "3.0.3" }, { "clone": 
"Packages/Alamofire-1.2.3", "origin": 
"https://github.com/a-fork-somewhere/Alamofire"; "ref": "crucial-fix" }, { 
"clone": "Packages/Quick-1.2.3", "origin": "https://github.com/Quick/Quick"; 
"ref": "1.2.3" } ] }

Workflow — Regular Build

User runs swift build
If Packages/ contains clones and a VersionLocks.jsonSwiftPM skips to 7.
If Packages/ contains clones and no VersionLocks.json the lockfile is generated 
from the clones
If Packages/ contains checked out sources without git information and no 
VersionLocks.json SwiftPM fetches the git information and provided there is no 
diff, generates the Lockfile, if there is variation it is an error *
If Packages/VersionLocks.json is present its dependency graph is used
If Packages doesn't exist or is empty the dependency graph is resolved, 
packages are cloned and the Lockfile is generated
Build, if Packages are missing because we skipped from 2. the build will error, 
it is the user's responsibility to instruct SwiftPM to --update or to fix their 
dependency graph some other way.

This scenario is so users can check in their complete dependency sources to 
their tree instead of / as well as the VersionLocks.json file: a situation 
which sometimes is necessary if your dependencies are removed from their third 
party online location, etc.

Workflow — Making Modifications

User makes local modification to a dependency’s sources
User runs swift build
swift build errors out.
User must either lock the graph or run with --ignore-lock
The error-out is likely to be considered tedious by users, however we consider 
it important that users are made aware and forced to act when they modify their 
dependencies and thus are exposing their team/users to so-called “dependency 
hell”.

Runing swift build --lock regenerates the lockfile, but does not build.

Modifications must be committed. This means that if the modifications are not 
uploaded to a location accessible to the rest of the team they will fail to 
build when they update their checkouts.

The package manager could check for this by asking git if the specified origin 
has the current locked ref and error out as appropriate.

Workflow — Overriding Packages

User steps into a Package directory eg. Packages/Foo-1.2.3
User changes the origin of Foo to their own fork
User alters HEAD to point to a fix in their own fork
swift build errors out.
User must either lock the graph or run with --ignore-lock
Running swift build --lock regenerates the lockfile, the new origin and tag is 
stored. Thus a fresh clone of this project would use these overrides.

It is important to note that this workflow will not be respected for 
dependencies, only for projects.

If a package author requires an override they have a few options:

Change the Package.swift dependency specification. This should only be done as 
a last resort, for example, a critical bug must be fixed in a dependency and 
that dependency author is not being responsive. It is up to the Package author 
to ensure this scenario goes well. SwiftPM itself wants to guard against these 
conditions with our proposed “publish & lint” step that validates such 
decisions before signing a published package tag. But we are not there yet and 
thus package authors should be responsible.
Advise end-users in a package README that they should override the dependency 
themselves.
2 is preferred, but 1 will happen. We consider it our responsibility to develop 
tooling that makes 1. safe or unnecessary, but we are not there yet.

Workflow — Updating Packages

SwiftPM has no update mechanism yet, but once it does running swift build 
--update will fetch the latest versions of all dependencies and update the 
lockfile.

Impact on existing code
This proposal will have no impact on existing code.

Alternatives Considered
One alternative is to allow mentioning refs in manifest file while declaring a 
dependency but as discussed in this 
<http://markdownlivepreview.com/%22https://lists.swift.org/pipermail/swift-build-dev/Week-of-Mon-20151214/>
 thread it might not be the best idea.

Using Git submodules for this feature was considered. However something 
additionally would be required to specify swift version and record local diffs. 
Also this would lock us into git, and despite the fact that currently we only 
use git, we have not yet ruled out supporting other version control systems.
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to