mirror of
https://github.com/semver/semver.git
synced 2026-07-11 03:53:53 -05:00
[GH-ISSUE #337] Public dependencies should be transitive #4488
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @dheld-expedia on GitHub (Oct 13, 2016).
Original GitHub issue: https://github.com/semver/semver/issues/337
SemVer should not apply to the first-order API of a versioned entity. Instead, it should apply to the transitive closure of public APIs produced and consumed by a versioned entity.
A dependency is "public" if consumers will share the dependency. Otherwise, it is "private". In Java, a JAR has public dependencies if is used as a library and requires other JARs to be on the classpath which a consuming system will also use (because it will share the classpath). A similar scenario exists for DLLs and Linux Shared Objects. A REST service API, on the other hand, will primarily only have private dependencies, because consumers of the service will unlikely be required to share whatever dependencies the REST service demands.
Why should we use the transitive closure of public dependencies? Because that is what consumers will see! A Java example:
In this example, the SemVer entity of interest is
MyLib-1.0. We see it has a dependency onGuava-18.0. Now, the consumer of my entity isConsumerApp-1.0. When we deployConsumerApp, we will collect all of the JARs that it requires and put them on a global classpath. Thus, whatever JARsMyLibneeds to function will also be seen byConsumerApp. So what happens when we do this:Hmm...now suppose that a previously unstated dependency was this:
When they were both at version 18, no problem! Now that we've bumped Guava to 19 for MyLib, what happens when ConsumerApp takes a dependency on
MyLib-1.1? Well, you end up with this:And now, the build for ConsumerApp will likely contain 2 major versions of Guava, depending whether it rejected the conflict or not.
The root problem is that the Guava major version should be considered part of the public API for MyLib. When it undergoes a major change, then MyLib must also get a major version bump. In fact, a major version bump at the leaf of any dependency tree must propagate a major bump all the way up the tree! Anything less will cause problems. Although this example used Java, it is not at all exclusive to Java. This will happen any time transitive dependencies can be shared.
Thus, when evaluating the set of changes to determine the scope of a new version, the transitive closure of public symbols must be considered. If the change includes a patch level increase in a deeply nested dependency, then the top-level version also requires a patch level increase. If a minor version increase in a public dependency appears, then the top-level version requires a minor version increase.
Note that private dependencies are excluded, because they should not interact. For instance, if a Java program loads a JAR/class at runtime in a separate classloader, then this will not conflict with other classes already loaded. That is a private dependency. However, if it loads a class in the same classloader as the rest of the program, then it may be a delay-loaded public dependency, and become part of the transitive dependency closure of the application.
@jeme commented on GitHub (Apr 25, 2017):
If i understand this issue correctly, I think it is unwise for SEMVER to dabble in such matters.
There is two reasons for that, first of all...
This would become really unmanageable really fast. (At least on .NET)
I already experience this heavily on .NET, how often don't we add binding redirects?...
I don't think it's uncommon to have more than 100 dependencies in total in a tree of dependencies. And it feels awkward that you would have to update your major because you depend on something you use internally, just because the platform is unable to keep that component isolated to your component (if you exchange objects from that dependency in your interface it is a different matter IMO).
I get the intent, and it's a real problem for sure, but I think it's a limitation of a given platform and I can't help but feel a bit odd about writing general rules for this. I can't wrap my head around this TBH, but perhaps I am wrong, perhaps it could be done without introducing even more chaos.
This is highly dependent on your platform.
As said before, it's a limitation of a given platform, so adding something to SEMVER that only applies to Platform X, Y and X but not A, B and C?...
AFAIK a platform such as NodeJS is fine with mixing versions because they are scoped (I think), where the CLR and JVM suffers unless you jump hoops. And you mention REST services your self as something that would not have this problem.
So this is a complex subject, and as such perhaps too complex for SEMVER to deal with??
@alexandrtovmach commented on GitHub (Jun 10, 2020):
This issue looks staled and will be closed in 10 days if there are no objections. Thanks everyone for contributions, you're amazing 🎆
@alexandrtovmach commented on GitHub (Jun 19, 2020):
👻 👻 👻
Closed as staled
@dagguh commented on GitHub (Jun 6, 2024):
Sharing a classpath is awkward, but it's also a real problem. Luckily, SemVer allows you to be a good API producer by including major transitive deps as part of your public API. This is also why Guava is such an awful lib to include in the transitive tree. The deeper problem here is the fact that most of the deps in the tree do not follow SemVer, so they're technically patch versions, but practically breaking.