|
| 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