Skip to content

Commit 91725ee

Browse files
committed
Initiate review of SE-0145 Package Manager Version Pinning
2 parents 040fe5e + 84e5e6d commit 91725ee

File tree

2 files changed

+311
-0
lines changed

2 files changed

+311
-0
lines changed

index.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
<proposal id="0142" status="accepted" swift-version="4" name="Permit where clauses to constrain associated types" filename="0142-associated-types-constraints.md"/>
146146
<proposal id="0143" status="active" swift-version="4" name="Conditional conformances" filename="0143-conditional-conformances.md"/>
147147
<proposal id="0144" status="rejected" swift-version="4" name="Allow Single Dollar Sign as a Valid Identifier" filename="0144-allow-single-dollar-sign-as-valid-identifier.md"/>
148+
<proposal id="0145" status="active" swift-version="4" name="Package Manager Version Pinning" filename="0145-package-manager-version-pinning.md"/>
148149

149150
<!--
150151
Recognized values for a proposal's status:
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# Package Manager Version Pinning
2+
3+
* Proposal: [SE-0145](0145-package-manager-version-pinning.md)
4+
* Author: [Daniel Dunbar](https://github.com/ddunbar), [Ankit Aggarwal](https://github.com/aciidb0mb3r), [Graydon Hoare](https://github.com/graydon)
5+
* Review Manager: [Anders Bertelrud](https://github.com/abertelrud)
6+
* Status: **Active Review (October 31...November 4)**
7+
8+
## Introduction
9+
10+
This is a proposal for adding package manager features to "pin" or "lock" package dependencies to particular versions.
11+
12+
## Motivation
13+
14+
As used in this proposal, version pinning refers to the practice of controlling
15+
*exactly* which specific version of a dependency is selected by the dependency
16+
resolution algorithm, *independent from* the semantic versioning
17+
specification. Thus, it is a way of instructing the package manager to select a
18+
particular version from among all of the versions of a package which could be
19+
chosen while honoring the dependency constraints.
20+
21+
### Terminology
22+
23+
*We have chosen to use "pinning" to refer to this feature, over "lockfiles", since
24+
the term "lock" is already overloaded between POSIX file locks and locks in
25+
concurrent programming.*
26+
27+
### Use Cases
28+
29+
Our proposal is designed to satisfy several different use cases for such a behavior:
30+
31+
1. Standardizing team workflows
32+
33+
When collaborating on a package, it can be valuable for team members (and
34+
continuous integration) to all know they are using the same exact version of
35+
dependencies, to avoid "works for me" situations.
36+
37+
This can be particularly important for certain kinds of open source projects
38+
which are actively being cloned by new users, and which want to have some
39+
measure of control around exactly which available version of a dependency is
40+
selected.
41+
42+
2. Difficult to test packages or dependencies
43+
44+
Complex packages which have dependencies which may be hard to test, or hard
45+
to analyze when they break, may choose to maintain careful control over what
46+
versions of their upstream dependencies they recommend -- even if
47+
conceptually they regularly update those recommendations following the true
48+
semantic version specification of the dependency.
49+
50+
3. Dependency locking w.r.t. deployment
51+
52+
When stabilizing a release for deployment, or building a version of a package
53+
for deployment, it is important to be able to lock down the exact versions of
54+
dependencies in use, so that the resulting product can be exactly recreated
55+
later if necessary.
56+
57+
### Mechanism and policy
58+
59+
This proposal primarily addresses the _mechanism_ used to record
60+
and manage version-pinning information, in support of _specific
61+
workflows_ with elevated demands for reproducible builds.
62+
63+
In addition to this, certain _policy_ choices around default
64+
behavior are included; these are set initially to different
65+
defaults than in many package managers. Specifically the default
66+
behavior is to _not_ generate pinning information unless
67+
requested, for reasons outlined in the alternatives discussion.
68+
69+
If the policy choice turns out to be wrong, the default can be
70+
changed without difficulty. We actively expect to revisit this policy
71+
choice once we have had experience with the mechanism, and can
72+
observe how it is being used.
73+
74+
### Current Behavior
75+
76+
The package manager *NEVER* updates a locally cloned package from its current
77+
version without explicit user action (`swift package update`). We anticipate
78+
encouraging users to update to newer versions of packages when viable, but this
79+
has not yet been proposed or implemented.
80+
81+
Whenever a package is operated on locally in a way that requires its
82+
dependencies be present (typically a `swift build`, but it could also be `swift
83+
package fetch` or any of several other commands), the package manager will fetch
84+
a complete set of dependencies. However, when it does so, it attempts to get
85+
versions of the missing dependencies compatible with the existing dependencies.
86+
87+
From a certain perspective, the package manager today is acting as if the local
88+
clones were "pinned", however, there has heretofore been no way to share that
89+
pinning information with other team members. This proposal is aimed at
90+
addressing that.
91+
92+
Thus, although our policy choice here is to not _generate_ pinning information
93+
by default in a location which is likely to be checked in, our local behavior on
94+
any individual developers desktop is always as if there is some pinning.
95+
96+
## Proposed solution
97+
98+
We will introduce support for an **optional** new file `Package.pins` adjacent
99+
to the `Package.swift` manifest, called the "pins file". We will also introduce
100+
a number of new commands (see below) for maintaining the pins file.
101+
102+
This file will record the active version pin information for the package,
103+
including data such as the package identifier, the pinned version, and explicit
104+
information on the pinned version (e.g., the commit hash/SHA for the resolved
105+
tag).
106+
107+
The exact file format is unspecified/implementation defined, however, in
108+
practice it will be a JSON data file.
109+
110+
This file *may* be checked into SCM by the user, so that its effects apply to
111+
all users of the package. However, it may also be maintained only locally (e.g.,
112+
placed in the `.gitignore` file). We intend to leave it to package authors to
113+
decide which use case is best for their project.
114+
115+
In the presence of a top-level `Package.pins` file, the package manager will
116+
respect the pinned dependencies recorded in the file whenever it needs to do
117+
dependency resolution (e.g., on the initial checkout or when updating).
118+
119+
The pins file will not override Manifest specified version requirements and it
120+
will be an error (with proper diagnostics) if there is a conflict between the pins
121+
and the manifest specification.
122+
123+
The pins file will also not influence dependency resolution for dependent packages;
124+
for example if application A depends on library B which in turn depends on library C,
125+
then package resolution for application A will use the manifest of library B to learn
126+
of the dependency on library C, but ignore any `Package.pins` file belonging to
127+
library B when deciding which version of library C to use.
128+
129+
## Detailed Design
130+
131+
1. We will add a new command `pin` to `swift package` tool with following semantics:
132+
133+
```
134+
$ swift package pin ( [--all] | [<package-name>] [<version>] ) [--message <message>]
135+
```
136+
137+
The `package-name` refers to the name of the package as specified in its manifest.
138+
139+
This command pins one or all dependencies. The command which pins a single version can optionally take a specific version to pin to, if unspecified (or with `--all`) the behavior is to pin to the current package version in use. Examples:
140+
* `$ swift package pin --all` - pins all the dependencies.
141+
* `$ swift package pin Foo` - pins `Foo` at current resolved version.
142+
* `$ swift package pin Foo 1.2.3` - pins `Foo` at 1.2.3. The specified version should be valid and resolvable.
143+
144+
The `--reason` option is an optional argument to document the reason for pinning a dependency. This could be helpful for user to later remember why a dependency was pinned. Example:
145+
146+
`$ swift package pin Foo --reason "The patch updates for Foo are really unstable and need screening."`
147+
148+
NOTE: When we refer to dependencies in the context of pinning, we are
149+
referring to *all* dependencies of a package, i.e. the transitive closure of
150+
its immediate dependencies specified in the package manifest. One of the
151+
important ways in which pinning is useful is because it allows specifying a
152+
behavior for the closure of the dependencies outside of them being named in
153+
the manifest.
154+
155+
156+
2. Dependencies are never automatically pinned, pinning is only ever taken as a result of an explicit user action.
157+
158+
3. We will add a new command `unpin`:
159+
160+
```
161+
$ swift package unpin ( [--all] | [<package-name>] )
162+
```
163+
This is the counterpart to the pin command, and unpins one or all packages.
164+
165+
4. We will fetch and resolve the dependencies when running the pin commands, in case we don't have the complete dependency graph yet.
166+
167+
5. We will extend the workflow for update to honor version pinning, that is, it will only update packages which are unpinned, and it will only update to versions which can satisfy the existing pins. The update command will, however, also take an optional argument `--repin`:
168+
169+
```
170+
$ swift package update [--repin]
171+
```
172+
173+
* Update command errors if there are no unpinned packages which can be updated.
174+
175+
* Otherwise, the behavior is to update all unpinned packages to the latest possible versions which can be resolved while respecting the existing pins.
176+
177+
* The `[--repin]` argument can be used to lift the version pinning restrictions. In this case, the behavior is that all packages are updated, and packages which were previously pinned are then repinned to the latest resolved versions.
178+
179+
6. The update and checkout will both emit logs, notifying the user that pinning is in effect.
180+
181+
7. The `swift package show-dependencies` subcommand will be updated to indicate if a dependency is pinned.
182+
183+
## Future Directions
184+
185+
We have intentionally kept the pin file format an implementation detail in order
186+
to allow for future expansion. For example, we would like to consider embedding
187+
additional information on a known tag (like its SHA, when using Git) in the pins
188+
file as a security feature, to prevent man-in-the-middle attacks on parts of the
189+
package graph.
190+
191+
## Impact on existing code
192+
193+
There will be change in the behaviors of `swift build` and `swift package update` in presence of the pins file, as noted in the proposal, however the existing package will continue to build without any modifications.
194+
195+
## Alternative considered
196+
197+
### Pin by default
198+
199+
Much discussion has revolved around a single policy-default question: whether
200+
SwiftPM should generate a pins file as a matter of course any time it
201+
builds. This is how some other package managers work, and it is viewed as a
202+
conservative stance with respect to making repeatable builds more likely between
203+
developers. Developers will see the pins file and will be likely to check it in
204+
to their SCM system as a matter of convention. As a side effect, other
205+
developers cloning and trying out the package will then end up using the same
206+
dependencies the developer last published.
207+
208+
While pinning does reduce the risk of packages failing to build, this practice
209+
discourages the community from relying on semver compatibility to completely
210+
specify what packages are compatible. That in turn makes it more likely for
211+
packages to fail to correctly follow the semver specification when publishing
212+
versions. Unfortunately, when packages don't correctly follow semver then it
213+
requires downstream clients to overspecify their dependency constraints since
214+
they cannot rely on the package manager automatically picking the appropriate
215+
version.
216+
217+
Overconstraint is much more of a risk in Swift than in other languages
218+
using this style of package management. Specifically: Swift does not support
219+
linking multiple versions of a dependency into the same artifact at the same
220+
time. Therefore the risk of producing a "dependency hell" situation, in which
221+
two packages individually build but _cannot be combined_ due to over-constrained
222+
transitive dependencies, is significantly higher than in other languages.
223+
Changing the compiler support in this area is not something which is currently
224+
planned as a feature, so our expectation is that we will have this limitation
225+
for a significant time.
226+
227+
For example, if package `Foo` depends on library `LibX` version 1.2,
228+
and package `Bar` depends on `LibX` 1.3, and these are _specific_
229+
version constraints that do not allow version-range variation,
230+
then SwiftPM will _not_ allow building a product that depends on
231+
both `Foo` and `Bar`: their requirements for `LibX` are incompatible.
232+
Where other package managers will simultaneously link two versions
233+
of `LibX` -- and hope that the differing simultaneous uses of
234+
`LibX` do not cause other compile-time or run-time errors --
235+
SwiftPM will simply fail to resolve the dependencies.
236+
237+
We therefore wish to encourage library authors to keep their
238+
packages building and testing with as recent and as wide a range
239+
of versions of their dependencies as possible, and guard more
240+
vigorously than other systems against accidental overconstraint.
241+
One way to encourage this behavior is to avoid emitting pins files
242+
by default.
243+
244+
We also believe that if packages default to exposing their pin files as part of
245+
their public package, there is a substantial risk that when developers encounter
246+
build failures they will default to copying parts of those pinned versions into
247+
their manifest, rather than working to resolve the semver specification issues
248+
in the dependencies. If this behavior becomes common place, it may even become
249+
standard practice to do this proactively, simply to avoid the potential of
250+
breakage.
251+
252+
This practice is likely because it resolves the immediate issue (a build
253+
failure) without need for external involvement, but if it becomes widespread
254+
then it has a side-effect of causing significant overconstraint of packages
255+
(since a published package may end up specifying only a single version it is
256+
compatible with).
257+
258+
Finally, we are also compelled by several pragmatic implications of an approach
259+
which optimizes for reliance on the semver specifications:
260+
261+
1. We do not yet have a robust dependency resolution algorithm we can rely
262+
on. The complexity of the algorithm is in some ways relative to the degree of
263+
conflicts we expect to be present in the package graph (for example, this may
264+
mean we need to investigate significantly more work in optimizing its
265+
performance, or in managing its diagnostics).
266+
267+
2. The Swift package manager and its ecosystem is evolving quickly, and we
268+
expect it will continue to do so for some time. As a consequence, we
269+
anticipate that packages will frequently be updated simply to take advantage
270+
of new features. Optimizing for an ecosystem where everyone can reliably live
271+
on the latest semver-compatible release of a package should help make that a
272+
smoother process.
273+
274+
If, in practice, the resulting ecosystem either contains too many packages that
275+
fail to build, or if a majority of users emit pins files manually regardless of
276+
default, this policy choice can be revisited.
277+
278+
We considered approachs to "pin by default" that used separate mechanisms when
279+
publishing a package to help address the potential for overconstraint, but were
280+
unable to find a solution we felt was workable.
281+
282+
283+
### Naming Choice
284+
285+
This feature is called "locking" and the files are "lockfiles" in many other
286+
package managers, and there has been considerable discussion around whether the
287+
Swift package manager should follow that precedent.
288+
289+
In Swift, we have tried to choose the "right" answer for names in order to make
290+
the resulting language consistent and beautiful.
291+
292+
We have found significant consensus that without considering the prededent, the
293+
"lock" terminology is conceptually the *wrong* word for the operation being
294+
performed here. We view pinning as a workflow-focused feature, versus the
295+
specification in the manifest (which is the "requirement"). The meaning of pin
296+
connotes this transient relationship between the pin action and the underlying
297+
dependency.
298+
299+
In constrast, not only does lock have the wrong connotation, but it also is a
300+
heavily overloaded word which can lead to confusion. For example, if the package
301+
manager used POSIX file locking to prevent concurrent manipulation of packages
302+
(a feature we intend to implement), and we also referred to the pinning files as
303+
"lock files", then any diagnostics using the term "lock file" would be confusing
304+
to a newcomer to the ecosystem familiar with the pinning mechanism but
305+
unfamiliar with the concept of POSIX file locking.
306+
307+
We believe that there are many more potential future users of the Swift package
308+
manager than there are current users familiar with the lock, and chose the "pin"
309+
terminology to reflect what we thought was ultimately the best word for the
310+
operation, in order to contribute to the best long term experience.

0 commit comments

Comments
 (0)