Search code examples
rubyrubygemsgemspecs

Best way to handle a direct dependency that another dependency uses in a gemspec file


In a Ruby project, I have a dependency that is used by my gem, and also used by a gem that my gem depends on. Specifically, I use sawyer and octokit, so my-gem depends on sawyer, and octokit also depends on sawyer.

Right now, this is what I have:

spec.add_runtime_dependency 'octokit', '~> 6.0'
spec.add_runtime_dependency 'sawyer'  # no version constraint

This works fine for now, but I'm wondering if this is the best way to do things. The intent is to use whatever version of sawyer that octokit uses, but I'm wondering if it is perhaps better to have a version constraint, or to just omit sawyer from the gemspec.


Solution

  • TL;DR

    Gems are most often meant to be libraries, not applications, although there are exceptions. Libraries should only list gems in their gemspecs that are first class dependencies, and avoid building in unnecessary or overly restrictive constraints on the gem.

    What to List and How Much to Constrain

    When to List and Constrain Gems

    As a rule of thumb, gems (as opposed to applications) shouldn't constrain gem dependencies except when necessary to avoid specific conflicts. Otherwise, you may unnecessarily constrain the use of your gem by others who have other constraints or need later or earlier versions for whatever reason.

    So, assuming your gem really needs octokit >= 6.0 and <= 6.1, then go ahead and constrain it that way. Otherwise, drop or reduce the level of constraint to the greatest practical extent. For example, if a dependency follows semantic versioning then you should only constrain its major version (if you constrain it at all) unless there's a known problem with a particular minor or patch version that you need to avoid. In other words, be as liberal as possible with library constraints.

    Don't Duplicate the Responsibilities of Bundler or Upstream Libraries

    If you aren't calling the sawyer gem directly, then it's really just a dependency of octokit that's unrelated to your gem. In such cases, you should trust Bundler's dependency resolution for octokit to pull in the things it needs to function unless you find a specific problem. Otherwise, you will end up managing dependencies that may change upstream, which creates a needless burden on both maintenance of your library and any developers who use it.

    On the other hand, if your gem makes direct calls to sawyer, then it's a first class dependency that should be listed in your gemspec. Again, if needed at all, keep your constraints as loose as you can to avoid excessive constraints or having to continuously update minor or patch versions down the road.

    In short, if the only reason to list sawyer is because octokit currently uses it then Bundler should manage the dependency. Ideally, the octokit maintainers should update octokit's dependencies when needed. Only manage third-party dependencies when absolutely necessary, and only as long as it takes to get essential fixes merged into the upstream library (or into a suitable fork or library replacement if the fixes you need won't be merged).