Some gems require external dependencies, such as gems containing C extensions
that compile against external C libraries (ex: pg) or gems that wrap around
external command-line utilities (ex: graphviz). Presently there is no
mechanism to automatically install these external dependencies. Instead, it is
the user's responsibility to figure out the correct package name and install the
external dependency via their system's package manager (ex: apt or brew)
and attempt installing the gem again. This often results in users becoming
frustrated when a C extension does not successfully build and then having to
search google and StackOverflow for which package needs to be installed.
Very rarely do gem authors list external dependencies via the gemspec
requirements attribute, and if so, it usually lists the canonical project
name of the external dependency (ex: sqlite3) and not the package name
(ex: libsqlite3-dev).
To workaround the problem of external dependencies some popular gems (ex:
nokogiri) have begun vendoring their external dependencies directly into the
gem and building both the C library and any C extensions during installation.
There are two downsides to this approach:
- Increased compilation time.
- Security Advisories: each time a new security advisory is published for the vendored library, the gem maintainer has to update the vendored copy of the C library, publish an updated version of the gem, and then publish their own security advisory warning users that the previous versions of their gem contains a vulnerable copy of the vendored library.
It should be possible to list the external dependencies required by a gem and have rubygems automatically install them from the system's package manager during the installation process.
This external_dependencies metadata could be embedded in the gemspec's
metadata attribute. In order to support naming differences between different
package managers, the package names for the external dependencies would need to
be listed for each popular package manager.
gemspec.metadata['external_dependencies'] = {
'apt' => %w[libsqlite3-dev],
'dnf' => %w[sqlite-devel],
'brew' => %w[sqlite],
# ...
}If only one external dependency needs to be specified, then the values of the
external_dependencies Hash could also be single package name Strings which
would later be automatically coerced into an Array:
gemspec.metadata['external_dependencies'] = {
'apt' => 'libsqlite3-dev',
'dnf' => 'sqlite-devel',
'brew' => 'sqlite',
# ...
}However, this might be a bit confusing?
If all of the package names are the same for each package manager,
then external_dependencies could be specified as an Array of Strings:
gemspec.metadata['external_dependencies'] = %w[nmap]Multiple package managers may be installed on the same system. In order to determine the primary system package manager, the system's package manager can be selected based on the OS/distro/flavor.
macOS is a unique edge-case since it does not have a default package manager. So there should be a prioritized list of macOS package managers to check for in order of popularity:
brewports
If gems can specify packages names that will be installed via an
apt-get install or brew install command, special concern should be made to
prevent arbitrary command injection. system() with multiple arguments
(ex: system('apt-get','install',...) or Shellwords.shellescape must be
used to prevent command injection.
In order to prevent option injection via the gemspec's package names, an
argument of '--' or '-' can be specified before the package names to
prematurely terminate option parsing and prevent the package names, so that
they are not accidentally parsed as options.
Some users may wish to customize which package manager is used on their system, if they have multiple package managers installed alongside each other. It may be necessary to add a configuration option or environment variable to control which package manager is used by default. Although, this feature request seems to be very rare.
If we allow annotating the external dependencies and automatically
installing them along with the gem, the gemspec's requirements attribute
might no longer be necessary and could be deprecated in the future?
Naming
I like "package dependency". Because this feature just installs one or more packages. This feature doesn't install a dependency by building it.
How to specify metadata
I think that we request a new attribute for this instead of reusing
metadata,requirementsand so on. Anyway, we can discuss this with RubyGems developers after we create a issue for this on rubygems/rubygems. I think that we can focus on what metadata should we need for this feature.What metadata are needed
I think that the following metadata are needed:
Here are some explains:
Package name key
I think that package manager name as key isn't suitable.
For example,
dnfis used by Fedora, Red Hat Enterprise Linux and Red Hat Enterprise Linux rebuild distributions (AlmaLinux, Rocky Linux and so on). But Fedora and Red Hat Enterprise Linux have many differences. For example, Fedora hasgtk4-develbut AlmaLinux 8 doesn't have it.Another example, ALT Linux uses
apt-getbut package names are different with Debian GNU/Linux. For example, ALT Linux useslibgtk+3-develfor GTK 3 but Debian GNU/Linux useslibgtk-3-devfor GTK 3.So I think that platform ID (
rhel,redhator something for Red Hat Enterprise Linux and its rebuilds) is better for key. This is why native-package-installer uses platform ID as key. We may need to accept platform version as optional value such asrhelandrhel-8.(We may resolve the above case by the "one of packages" feature described in mysql2 gem case. One of
libmysqlclientandlibmariadbis required in the case.)How to specify "one of packages"
I agree to the "check for package A, otherwise install package B" with users can specify which is used explicitly approach.
How to detect already installed packages
I agree to detection by
pkg-configcommand. But I suggestpkg-configinstead ofpkgfor key. Because we may add more information to detect installed packages later. For example, we may use library name such asxmlthat findslibxml.soon Linux andlibxml.dylibon macOS.For toolchain and
pkg-configcommand, I think that we don't need to install them automatically. I think that it's already installed byruby-dev/ruby-develpackage or RVM/rbenv. Ifpkg-configcommand isn't installed, we can skip installed packages check bypkg-configcommand. Or we can add a new RubyGems option to installpkg-configcommand automatically if it doesn't exist.How to install package
Some packages need extra action to be installed.
For example, we need to enable
powertoolsrepository to installsnappy-develon AlmaLinux 8:We need to enable
crbrepository to installsnappy-develon AlmaLinux 9:Another example, we need to install
epel-releaseto installre2-develon AlmaLinux 8 and AlmaLinux 9:But
epel-releasepackage doesn't exist in Red Hat Enterprise Linux 8. We need to install it from https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm :Amazon Linux 2 (based on Red Hat Enterprise Linux 7) uses another command line:
Ubuntu has PPA (Personal Package Archive) mechanism: https://help.launchpad.net/Packaging/PPA
Users can add an external package repository by
add-apt-repository:As a personal request, I want a feature that registers external APT/Yum repository by
.deb/.rpmURL. e.g.:Because this is one of my use cases.
Scenarios
Scenario 1: runtime-only dependency
I'm using
native-package-installer/pkg-configfor this scenario with theRakefileapproach mentioned by @postmodern. (I didn't think that this is abuse...)For example,
red-parquet:Scenario 2: Simple C extension
I'll add metadata to
.gemspecand keep the auto package install feature inextconf.rbuntil all RubyGems that don't support the feature reach EOL. But I'm happy with the situation. Because I can remove the auto package install feature later.I'm OK with putting package information to
.gemspecandextconf.rb. I understand that we don't want to spread the information to multiple places. But we can read.gemspecfromextconf.rband refer the information in.gemspecinextconf.rbif needed.Scenario 3.a: C extension with "use whichever" optionality
(See my comment in 'How to specify "one of packages"'.)
Scenario 3.b: C extension with "choose one at install time" optionality
Hmm, it may be better that sqlite3-ruby is split to sqlite3-ruby and sqlcipher-ruby. Then users can install
libsqlite3bindings andlibsqlcipherbindings at once.LGPL
As I mentioned sparklemotion/nokogiri#1488 (comment) , people who objected misunderstand LGPL.
In addition, in the Nokogiri use case,
extconf.rbis only "Application" and "Combined Work" in LGPLv3.lib/**/*.rbandnokogiri.soare neither "Application" nor "Combined Work" in LGPLv3. So applications which use onlylib/**/*.rbandnokogiri.sodoesn't related to LGPLv3.Anyway... "install-time dependency" will satisfy people who objected
native-package-installer/pkg-configuse.Alternative: RubyGems plugin
We may be able to implement this feature by RubyGems' plugin feature: https://guides.rubygems.org/plugins/
For example, https://github.com/voxik/gem-nice-install has similar feature.
But this requires users to install a RubyGems plugin explicitly before users install the target gem.