Dependencies resolution issue: unstable-version reported when using revision

Hello guys,

I am facing a strange behavior this morning.
In my company, we have a policy of relying on sementic versions for our internal packages, but on commits when relying on an external package. This is to make sure no unseen code change is used when compiling our products.

Today, we have

  • package Internal-1 that depends on Internal-2, with policy .upToNextMajor("1.2.3")
  • package Internal-2 that depends on External-1, with policy .revision("ABCDEF")

The resolution of the graph fails with this error:

Dependencies could not be resolved because root depends on 'Internal-2' 1.2.3..<2.0.0.
'Internal-2' >= 1.2.3 cannot be used because package 'Internal-2' is required using a stable-version but 'Internal-2' depends on an unstable-version package 'External-1' and no versions of 'Internal-2' match the requirement 1.2.4..<2.0.0.

This problem is fixed by using the .upToNextMajor policy for External-1 instead of .revision.

Reading the code, it is clearly an expected behavior (see PubgrubDependencyResolver.swift:1202), but it sounds pretty strange to me: a commit hash is even more stable than a tag, and using more stable dependency policies should not cause problems, don't you think ?

Would it be a valuable evolution proposal, to consider that a version-stable packages can depend on a stable-version OR a specific revision ?

A commit hash will never point at anything else, but unlike a semantic version tag, it carries no implication that it will continue to exist. In the second sense it is far less stable. So I think the guard rail is justified.

I recommend using an exact version for this purpose (instead of from).

Simply checking in the .resolved file is probably even better for most users. That is precisely what it is designed for, and it will be more lenient to your clients. The only thing it lacks compared to the exact strategy is that a manual change to one dependency can send a ripple effect through the graph. Whether you find it helpful that it found a working combination for you, or annoying that it changed pins you had not looked at, that is for you to decide.

I understand your point SDGGiesbrecht.
The problem I have with using an exact version is that a git tag can easily be changed to point to another commit. The stability of a tag (both in its existence and the revision it points to) is just a convention. If one of our dependencies gets hacked, and the tag is modified to a commit that integrates unexpected code, the only protection we have is the hash that is stored in the Package.resolved file, as you mentionned.
But this file is updated quite often, and notably in Xcode, if you right-click a dependency to update it, all dependencies will be updated. So this protection is not enough in my point of view.

What I would like to have is a way to mix flexibility of version-based dependency for our internal SPM and security of hash-based dependency for our external ones.

Would there be a negative side effect in considering a hash as stable during resolution, aside from limiting the possibilities of the graph resolver ?
How could we offer both a security-focused option, and a flexibility/ease of use-focused one ?

SwiftPM records security fingerprints permanently in your home folder (see ~/.swiftpm/security, although on some platforms it is a symlink to elsewhere). If SwiftPM ever attempts to fetch the same version again but receives different content, it will throw an error (which looks like this). If this per‐user memory is not broad enough, you can persist the security folder across CI runs, sync it across workstations, etc.