[GH-ISSUE #14] how to handle transitive dependencies #6000

Closed
opened 2026-06-17 04:23:58 -05:00 by GiteaMirror · 18 comments
Owner

Originally created by @motlin on GitHub (Feb 8, 2012).
Original GitHub issue: https://github.com/semver/semver/issues/14

Lets say you update versions of libraries your project depends on and make no other changes. When you re-release your project, which version number should you bump? I think it ought to be the minor number and that it shouldn't depend on whether those libraries broke backwards compatibility. I'm curious what others think.

If there's a consensus, I think it should be covered by the semver document.

Originally created by @motlin on GitHub (Feb 8, 2012). Original GitHub issue: https://github.com/semver/semver/issues/14 Lets say you update versions of libraries your project depends on and make no other changes. When you re-release your project, which version number should you bump? I think it ought to be the minor number and that it shouldn't depend on whether those libraries broke backwards compatibility. I'm curious what others think. If there's a consensus, I think it should be covered by the semver document.
Author
Owner

@rickhall commented on GitHub (Feb 15, 2012):

I think it depends.

If the dependency is exposed by your own API, then you may be forced to make a major bump, but if it is not exposed then a minor bump would be fine.

For example, assume I were programming in Java and my public API imported library Foo version 1.0.0 and exposed a Foo type in one of its turn value. Then later I updated my dependency to Foo 2.0.0 when it was released because my project only depended upon a subset of Foo's API that was still backward compatible. By doing so, I'll potentially be impacting clients of my API that are expecting a Foo 1.0.0 return type from one of methods.

How easily/often this could happen in the wild is open for debate, but it is certainly possible in Java.

<!-- gh-comment-id:3984362 --> @rickhall commented on GitHub (Feb 15, 2012): I think it depends. If the dependency is exposed by your own API, then you may be forced to make a major bump, but if it is not exposed then a minor bump would be fine. For example, assume I were programming in Java and my public API imported library Foo version 1.0.0 and exposed a Foo type in one of its turn value. Then later I updated my dependency to Foo 2.0.0 when it was released because my project only depended upon a subset of Foo's API that was still backward compatible. By doing so, I'll potentially be impacting clients of my API that are expecting a Foo 1.0.0 return type from one of methods. How easily/often this could happen in the wild is open for debate, but it is certainly possible in Java.
Author
Owner

@motlin commented on GitHub (Feb 16, 2012):

Keep in mind, if upgrading your dependency forces you to change your public API, you have to bump your major version anyway - that's already covered by the other rules.

<!-- gh-comment-id:3992459 --> @motlin commented on GitHub (Feb 16, 2012): Keep in mind, if upgrading your dependency forces you to change your public API, you have to bump your major version anyway - that's already covered by the other rules.
Author
Owner

@rickhall commented on GitHub (Feb 16, 2012):

Yes, certainly, but it is possible to change dependencies without changing API if it depends on a subset of the required library which is still backward compatible.

This is possible in languages like Java where I can use use one class at a time (even effectively one method at a time) from a potential library due to the late binding of class loaders.

As long as dependencies aren't exposed through your public API, then you can do anything you want with your dependencies because they are implementation details. Once they are exposed (e.g., via return types or method parameters), then you have to be more careful when changing your dependencies, since they effectively become part of your public API too.

<!-- gh-comment-id:4004863 --> @rickhall commented on GitHub (Feb 16, 2012): Yes, certainly, but it is possible to change dependencies without changing API if it depends on a subset of the required library which is still backward compatible. This is possible in languages like Java where I can use use one class at a time (even effectively one method at a time) from a potential library due to the late binding of class loaders. As long as dependencies aren't exposed through your public API, then you can do anything you want with your dependencies because they are implementation details. Once they are exposed (e.g., via return types or method parameters), then you have to be more careful when changing your dependencies, since they effectively become part of your public API too.
Author
Owner

@motlin commented on GitHub (Feb 16, 2012):

It sounds like we agree then. If you upgrade a dependency that doesn't affect your public API, the next release should be a minor release.

This wasn't obvious to me at first, so I think it's worth highlighting in the README with a sentence or two.

<!-- gh-comment-id:4007427 --> @motlin commented on GitHub (Feb 16, 2012): It sounds like we agree then. If you upgrade a dependency that doesn't affect your public API, the next release should be a minor release. This wasn't obvious to me at first, so I think it's worth highlighting in the README with a sentence or two.
Author
Owner

@rickhall commented on GitHub (Feb 16, 2012):

Perhaps we do agree, but your original message seems to indicate that you think you can ignore whether dependencies introduce breaking changes and always just do a minor bump in your API. I am saying this isn't true and it depends.

Your dependencies are potentially part of your public API if they are exposed via it. Thus, even if you don't change your public API itself, changing the dependencies could introduce breaking changes that require a major bump for dependencies exposed via your public API.

<!-- gh-comment-id:4008736 --> @rickhall commented on GitHub (Feb 16, 2012): Perhaps we do agree, but your original message seems to indicate that you think you can ignore whether dependencies introduce breaking changes and always just do a minor bump in your API. I am saying this isn't true and it depends. Your dependencies are potentially part of your public API if they are exposed via it. Thus, even if you don't change your public API itself, changing the dependencies could introduce breaking changes that require a major bump for dependencies exposed via your public API.
Author
Owner

@motlin commented on GitHub (Feb 17, 2012):

Here's an example to make it concrete. I'm releasing my library, which depends on LibraryXYZ. I have a method on my public API which returns InterfaceXYZ. If that interface adds a new method, I can still do a minor release. But if have a public interface which extends that interface, I must do a major release.

<!-- gh-comment-id:4020978 --> @motlin commented on GitHub (Feb 17, 2012): Here's an example to make it concrete. I'm releasing my library, which depends on LibraryXYZ. I have a method on my public API which returns InterfaceXYZ. If that interface adds a new method, I can still do a minor release. But if have a public interface which extends that interface, I must do a major release.
Author
Owner

@rickhall commented on GitHub (Feb 17, 2012):

Yes, I agree with your scenario, but it is more subtle than that.

What if they've changed InterfaceXYZ in an incompatible way, such as adding another argument to an existing method that previously only required one? My API doesn't need to change, but clients of me will now be getting a version of InterfaceXYZ with a method that requires two arguments instead of one. This will break them if they use that method.

<!-- gh-comment-id:4021577 --> @rickhall commented on GitHub (Feb 17, 2012): Yes, I agree with your scenario, but it is more subtle than that. What if they've changed InterfaceXYZ in an incompatible way, such as adding another argument to an existing method that previously only required one? My API doesn't need to change, but clients of me will now be getting a version of InterfaceXYZ with a method that requires two arguments instead of one. This will break them if they use that method.
Author
Owner

@motlin commented on GitHub (Feb 17, 2012):

Right. If they depend on the API of InterfaceXYZ then their build will need
to reflect that by using a specific version of LibraryXYZ. If they just get
the dependency transitively, they'll have problems.

On Fri, Feb 17, 2012 at 10:09 AM, rickhall <
reply@reply.github.com

wrote:

Yes, I agree with your scenario, but it is more subtle than that.

What if they've changed InterfaceXYZ in an incompatible way, such as
adding another argument to an existing method that previously only required
one? My API doesn't need to change, but clients of me will now be getting a
version of InterfaceXYZ with a method that requires two arguments instead
of one. This will break them if they use that method.


Reply to this email directly or view it on GitHub:
https://github.com/mojombo/semver/issues/14#issuecomment-4021577

<!-- gh-comment-id:4021644 --> @motlin commented on GitHub (Feb 17, 2012): Right. If they depend on the API of InterfaceXYZ then their build will need to reflect that by using a specific version of LibraryXYZ. If they just get the dependency transitively, they'll have problems. On Fri, Feb 17, 2012 at 10:09 AM, rickhall < reply@reply.github.com > wrote: > > Yes, I agree with your scenario, but it is more subtle than that. > > What if they've changed InterfaceXYZ in an incompatible way, such as > adding another argument to an existing method that previously only required > one? My API doesn't need to change, but clients of me will now be getting a > version of InterfaceXYZ with a method that requires two arguments instead > of one. This will break them if they use that method. > > --- > > Reply to this email directly or view it on GitHub: > https://github.com/mojombo/semver/issues/14#issuecomment-4021577
Author
Owner

@rickhall commented on GitHub (Feb 17, 2012):

Right. But, specifically, you've broken your original contract, since you were originally exposing them to InterfaceXYZ 1.0 and later switched to InterfaceXYZ 2.0. Thus, it is part of your own public API and would require a major version bump, even though your API didn't change.

In OSGi, for example, you could try to give your library only a minor version bump, but the dependency resolver would detect the incompatibility. Your updated library would be importing InterfaceXYZ 2.0 and the client library would be importing InterfaceXYZ 1.0 and since your public API has a "uses" constraint on the interface (i.e., it exposes it in its return type), then this would be detected and flagged as a dependency resolution error. Which just goes to prove that it wasn't a backward compatible change, since it is no longer can be used to resolve the existing client's dependency.

Of course, if a client wasn't using InterfaceXYZ at all, then they wouldn't be impacted, and for such a client the change would actually be backward compatible. Fun, huh?

<!-- gh-comment-id:4021910 --> @rickhall commented on GitHub (Feb 17, 2012): Right. But, specifically, you've broken your original contract, since you were originally exposing them to InterfaceXYZ 1.0 and later switched to InterfaceXYZ 2.0. Thus, it is part of your own public API and would require a major version bump, even though your API didn't change. In OSGi, for example, you could try to give your library only a minor version bump, but the dependency resolver would detect the incompatibility. Your updated library would be importing InterfaceXYZ 2.0 and the client library would be importing InterfaceXYZ 1.0 and since your public API has a "uses" constraint on the interface (i.e., it exposes it in its return type), then this would be detected and flagged as a dependency resolution error. Which just goes to prove that it wasn't a backward compatible change, since it is no longer can be used to resolve the existing client's dependency. Of course, if a client wasn't using InterfaceXYZ at all, then they wouldn't be impacted, and for such a client the change would actually be backward compatible. Fun, huh?
Author
Owner

@motlin commented on GitHub (Feb 18, 2012):

The fact that a client could avoid using InterfaceXYZ by not calling that
method is why it should be a minor number bump. If they do use it (in OSGi
or otherwise) they need to declare that dependency at compile time anyway
and they'll be forced to upgrade accordingly.

Another way of saying the same thing is - if it doesn't break a Clirr
build, it's a minor number bump.

On Fri, Feb 17, 2012 at 10:25 AM, rickhall <
reply@reply.github.com

wrote:

Right. But, specifically, you've broken your original contract, since you
were originally exposing them to InterfaceXYZ 1.0 and later switched to
InterfaceXYZ 2.0. Thus, it is part of your own public API and would require
a major version bump, even though your API didn't change.

In OSGi, for example, you could try to give your library only a minor
version bump, but the dependency resolver would detect the incompatibility.
Your updated library would be importing InterfaceXYZ 2.0 and the client
library would be importing InterfaceXYZ 1.0 and since your public API has a
"uses" constraint on the interface (i.e., it exposes it in its return
type), then this would be detected and flagged as a dependency resolution
error. Which just goes to prove that it wasn't a backward compatible
change, since it is no longer can be used to resolve the existing client's
dependency.

Of course, if a client wasn't using InterfaceXYZ at all, then they
wouldn't be impacted, and for such a client the change would actually be
backward compatible. Fun, huh?


Reply to this email directly or view it on GitHub:
https://github.com/mojombo/semver/issues/14#issuecomment-4021910

<!-- gh-comment-id:4031118 --> @motlin commented on GitHub (Feb 18, 2012): The fact that a client could avoid using InterfaceXYZ by not calling that method is why it should be a minor number bump. If they do use it (in OSGi or otherwise) they need to declare that dependency at compile time anyway and they'll be forced to upgrade accordingly. Another way of saying the same thing is - if it doesn't break a Clirr build, it's a minor number bump. On Fri, Feb 17, 2012 at 10:25 AM, rickhall < reply@reply.github.com > wrote: > > Right. But, specifically, you've broken your original contract, since you > were originally exposing them to InterfaceXYZ 1.0 and later switched to > InterfaceXYZ 2.0. Thus, it is part of your own public API and would require > a major version bump, even though your API didn't change. > > In OSGi, for example, you could try to give your library only a minor > version bump, but the dependency resolver would detect the incompatibility. > Your updated library would be importing InterfaceXYZ 2.0 and the client > library would be importing InterfaceXYZ 1.0 and since your public API has a > "uses" constraint on the interface (i.e., it exposes it in its return > type), then this would be detected and flagged as a dependency resolution > error. Which just goes to prove that it wasn't a backward compatible > change, since it is no longer can be used to resolve the existing client's > dependency. > > Of course, if a client wasn't using InterfaceXYZ at all, then they > wouldn't be impacted, and for such a client the change would actually be > backward compatible. Fun, huh? > > --- > > Reply to this email directly or view it on GitHub: > https://github.com/mojombo/semver/issues/14#issuecomment-4021910
Author
Owner

@rickhall commented on GitHub (Feb 18, 2012):

What you are saying doesn't really make sense.

A client using InterfaceXYZ would be compiling against InterfaceXYZ 1.0 and would have declared a dependency on that, which would work fine when they were using your public API 1.0 that also used and exposed InterfaceXYZ 1.0. Then if you go and change your dependency to InterfaceXYZ 2.0 and only update your version to 1.1, this would indicate that the client can use your 1.1 release because you are stating it is backward compatible.

But, as I pointed out, the client actually cannot use it since you are now exposing them to InterfaceXYZ 2.0, which is not compatible with what you were exposing them to in your version 1.0 (which was InterfaceXYZ 1.0). So, no, you can't just do a minor bump in that case.

The other case I pointed out about how a client may not call an incompatible method just makes it not a breaking change for that particular client, but your version numbering has to take into account the situation for all clients, not just idiosyncratic clients. In other words, if your changes break any client, then they are backward incompatible period.

<!-- gh-comment-id:4031869 --> @rickhall commented on GitHub (Feb 18, 2012): What you are saying doesn't really make sense. A client using InterfaceXYZ would be compiling against InterfaceXYZ 1.0 and would have declared a dependency on that, which would work fine when they were using your public API 1.0 that also used and exposed InterfaceXYZ 1.0. Then if you go and change your dependency to InterfaceXYZ 2.0 and only update your version to 1.1, this would indicate that the client can use your 1.1 release because you are stating it is backward compatible. But, as I pointed out, the client actually cannot use it since you are now exposing them to InterfaceXYZ 2.0, which is not compatible with what you were exposing them to in your version 1.0 (which was InterfaceXYZ 1.0). So, no, you can't just do a minor bump in that case. The other case I pointed out about how a client may not call an incompatible method just makes it not a breaking change for that particular client, but your version numbering has to take into account the situation for all clients, not just idiosyncratic clients. In other words, if your changes break any client, then they are backward incompatible period.
Author
Owner

@motlin commented on GitHub (Feb 18, 2012):

Well I think that's redundant. Clients that use both APIs already have all the information they need from the major number bump in LibraryXYZ.

Anyway, we've fleshed out the same two sides of the argument that I heard before. I'm curious what @mojombo thinks and whether it's worth including in semver.org.

<!-- gh-comment-id:4032209 --> @motlin commented on GitHub (Feb 18, 2012): Well I think that's redundant. Clients that use both APIs already have all the information they need from the major number bump in LibraryXYZ. Anyway, we've fleshed out the same two sides of the argument that I heard before. I'm curious what @mojombo thinks and whether it's worth including in semver.org.
Author
Owner

@rickhall commented on GitHub (Feb 18, 2012):

I don't see any redundancy here. From my point of view, it seems that you feel that exposing InterfaceXYZ from your own public API doesn't make it part of your public API, but I feel that it does make it part of your public API. That appears to be the crux of the issue. Our views are clear, other opinions are certainly welcome.

<!-- gh-comment-id:4035851 --> @rickhall commented on GitHub (Feb 18, 2012): I don't see any redundancy here. From my point of view, it seems that you feel that exposing InterfaceXYZ from your own public API doesn't make it part of your public API, but I feel that it does make it part of your public API. That appears to be the crux of the issue. Our views are clear, other opinions are certainly welcome.
Author
Owner

@motlin commented on GitHub (Feb 20, 2012):

In this example, InterfaceXYZ is obviously part of the public API. I see it as a question of whether the major number bump of LibraryXYZ conveys enough information or whether our library also needs to bump its major version.

<!-- gh-comment-id:4047445 --> @motlin commented on GitHub (Feb 20, 2012): In this example, InterfaceXYZ is obviously part of the public API. I see it as a question of whether the major number bump of LibraryXYZ conveys enough information or whether our library also needs to bump its major version.
Author
Owner

@rickhall commented on GitHub (Feb 20, 2012):

Ok, I see. Then for me there is no doubt that it isn't enough for our concocted scenario, because it may break some clients while a minor bump clearly indicates it shouldn't. I agree that this is a gray area; however, from my point of view, if you break any clients you are not backward compatible, no matter the reason for the breakage.

<!-- gh-comment-id:4048328 --> @rickhall commented on GitHub (Feb 20, 2012): Ok, I see. Then for me there is no doubt that it isn't enough for our concocted scenario, because it may break some clients while a minor bump clearly indicates it shouldn't. I agree that this is a gray area; however, from my point of view, if you break any clients you are not backward compatible, no matter the reason for the breakage.
Author
Owner

@TheRealEdwardCullen commented on GitHub (Jul 27, 2012):

This is a Hard Problem as much of it depends on implementation detail (such as linker capabilities).

I would go back to the question "what do I guarantee?" - that's what SemVer is about.

A real example I ran into was (on Linux) we had two different versions of the same library being loaded at the same time, resulting in a very confused linker... The solution on this occasion was to eliminate the duplicate...

I think that on Linux you can get around this kind of problem by using dlopen(3) and/or versioned symbols (but only works if the 3rd party lib has used it on both versions!)

Ultimately, I think this is outside the scope of SemVer, but may be a good item to include in the FAQ, even if it's just to say "this is an implementation question"...

<!-- gh-comment-id:7303251 --> @TheRealEdwardCullen commented on GitHub (Jul 27, 2012): This is a Hard Problem as much of it depends on implementation detail (such as linker capabilities). I would go back to the question "what do I _guarantee_?" - that's what SemVer is about. A real example I ran into was (on Linux) we had two different versions of the same library being loaded at the same time, resulting in a very confused linker... The solution on this occasion was to eliminate the duplicate... I _think_ that on Linux you can get around this kind of problem by using dlopen(3) and/or versioned symbols (but only works if the 3rd party lib has used it on both versions!) Ultimately, I think this is outside the scope of SemVer, but may be a good item to include in the FAQ, even if it's just to say "this is an implementation question"...
Author
Owner

@haacked commented on GitHub (Dec 20, 2012):

Sorry for the very late response. I agree, this is outside of the scope.

We can consider adding a FAQ with this (and other) questions here https://github.com/mojombo/semver.org

<!-- gh-comment-id:11556349 --> @haacked commented on GitHub (Dec 20, 2012): Sorry for the very late response. I agree, this is outside of the scope. We can consider adding a FAQ with this (and other) questions here https://github.com/mojombo/semver.org
Author
Owner

@haacked commented on GitHub (Dec 20, 2012):

After I wrote that comment, I realized the spec has a FAQ section. Doh! If you have a suggestion for how to capture this in a clear manner, please do submit a pull request. You can do it via editing the file in the web interface so there's no need to actually use git to clone it.

<!-- gh-comment-id:11556441 --> @haacked commented on GitHub (Dec 20, 2012): After I wrote that comment, I realized the spec has a FAQ section. Doh! If you have a suggestion for how to capture this in a clear manner, please do submit a pull request. You can do it via editing the file in the web interface so there's no need to actually use git to clone it.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/semver#6000