This gem is part of the Ruby C Extensions Explained project at https://github.com/flavorjones/ruby-c-extensions-explained
Many third-party libraries have their own build process based on tools like "autoconf" or "cmake". For those libraries, it might be really challenging to just copy the C files and let MakeMakefile compile them. In those situations, it's possible to package the entire upstream tarball and use that at installation time.
An important tool that we rely on for building third-party libraries is flavorjones/mini_portile
. It is able to configure a tarball distribution that relies on "autoconf" or "cmake" and compile it into a library that we can link against.
We add the tarball to the gemspec so that it's packaged with the rest of the gem:
Gem::Specification.new do |spec|
# ...
spec.files << "ports/archives/yaml-0.2.5.tar.gz"
# ...
end
The extconf.rb
is about to get more complicated. To help manage the complexity, we'll use a module with a .configure
method:
module RCEE
module PackagedTarball
module ExtConf
class << self
def configure
configure_packaged_libraries
create_makefile("rcee/packaged_tarball/packaged_tarball")
end
end
end
end
end
RCEE::PackagedTarball::ExtConf.configure
The ExtConf
contains a method configure_packaged_libraries
which uses mini_portile
to download the library (if necessary); verify the checksum; extract the files; configure, compile, and link it; and finally configure MakeMakefile
to find the headers and the library:
def configure_packaged_libraries
recipe = libyaml_recipe
# ensure libyaml has already been unpacked, configured, and compiled
unless File.exist?(File.join(recipe.target, recipe.host, recipe.name, recipe.version))
recipe.cook
end
# use the packaged libyaml
recipe.activate
pkg_config(File.join(recipe.path, "lib", "pkgconfig", "yaml-0.1.pc"))
# assert that we can build against the packaged libyaml
unless have_library("yaml", "yaml_get_version", "yaml.h")
abort("\nERROR: *** could not find libyaml development environment ***\n\n")
end
end
def libyaml_recipe
MiniPortile.new("yaml", "0.2.5").tap do |recipe|
recipe.files = [{
url: "https://github.com/yaml/libyaml/releases/download/0.2.5/yaml-0.2.5.tar.gz",
sha256: "c642ae9b75fee120b2d96c712538bd2cf283228d2337df2cf2988e3c02678ef4"
}]
recipe.target = File.join(PACKAGE_ROOT_DIR, "ports")
end
end
Of special note, we're able to use pkgconfig
to do much of our extension configuration, which allows us to have a pretty clean configuration. This is one advantage to using upstream distributions.
Note that all of those steps happen before the Makefile is created, so that the Makefile will contain the appropriate paths and flags to build our extension against libyaml.
See .github/workflows/packaged_source.yml
Key things to note:
- matrix across all supported Rubies and platforms
- caching the compiled library speeds up builds as well as avoids re-downloading the tarball over and over
- note that the cache key includes the platform and the hash of extconf.rb
If you run gem install rcee_packaged_tarball
and see this in action, You'll note that PackagedTarball is significantly slower than PackagedSource, and that's because MiniPortile is running additional configuration steps to build the library.
Maintainers now have an additional responsibility to keep that library up-to-date and secure for your users.