RCEE::PackagedSource
This gem is part of the Ruby C Extensions Explained project at https://github.com/flavorjones/ruby-c-extensions-explained
Summary
Some gems, like nokogumbo and psych, include the third-party library's C files.
This means the gem is redistributing the library -- so be careful with licensing implications if you do this. Packaging third-party libraries is a little more work for gem maintainers to set up, but may simplify your codebase and make installation more reliable.
Details
This gem has simply copied the source files from libyaml
, along with that library's LICENSE file, into the extensions directory:
ext/
└── packaged_source
├── extconf.rb
├── packaged_source.c
├── packaged_source.h
└── yaml
├── LICENSE
├── api.c
├── config.h
├── dumper.c
├── emitter.c
├── loader.c
├── parser.c
├── reader.c
├── scanner.c
├── writer.c
├── yaml.h
└── yaml_private.h
The extconf.rb
contains some new code:
# $VPATH is used as the Makefile's $(VPATH) variable
# see https://www.gnu.org/software/make/manual/html_node/General-Search.html
$VPATH << "$(srcdir)/yaml"
# $srcs is normally set to the list of C files in the extension directory,
# but we need to append the libyaml files to it.
$srcs = Dir.glob("#{$srcdir}/{,yaml/}*.c").map { |n| File.basename(n) }.sort
# and make sure that the C preprocessor includes the yaml directory in its search path
append_cppflags("-I$(srcdir)/yaml")
# assert that we can find the yaml.h header file
abort("could not find yaml.h") unless find_header("yaml.h")
# defines HAVE_CONFIG_H macro
have_header("config.h")
It first configures MakeMakefile
to pay attention to the ./yaml
directory as well as the C and header files. It then verifies that yaml.h
can be found (though we could probably skip this step). Finally, a libyaml-specific action is taken which is to make sure config.h
can be found and that the HAVE_CONFIG_H
macro is set so that yaml.h
is compiled properly.
The Makefile
recipe looks something like:
# `create_makefile` recipe is something like this
# compile phase:
gcc -c -I/path/to/ruby/include -I/path/to/libyaml/include packaged_source.c -o packaged_source.o
gcc -c -I/path/to/ruby/include -I/path/to/libyaml/include yaml/api.c -o api.o
gcc -c -I/path/to/ruby/include -I/path/to/libyaml/include yaml/dumper.c -o dumper.o
gcc -c -I/path/to/ruby/include -I/path/to/libyaml/include yaml/emitter.c -o emitter.o
gcc -c -I/path/to/ruby/include -I/path/to/libyaml/include yaml/loader.c -o loader.o
gcc -c -I/path/to/ruby/include -I/path/to/libyaml/include yaml/parser.c -o parser.o
gcc -c -I/path/to/ruby/include -I/path/to/libyaml/include yaml/reader.c -o reader.o
gcc -c -I/path/to/ruby/include -I/path/to/libyaml/include yaml/scanner.c -o scanner.o
gcc -c -I/path/to/ruby/include -I/path/to/libyaml/include yaml/writer.c -o writer.o
# link phase:
gcc -shared \
-L/path/to/ruby/lib -lruby \
-lyaml \
-lc -lm \
packaged_source.o api.o dumper.o emitter.o loader.o parser.o reader.o scanner.o writer.o \
-o packaged_source.so
Now we're able to rely on a specific version of the library existing with known configuration, allowing us to avoid much real-world complexity in our extconf.rb
, and our code.
Testing
See .github/workflows/packaged_source.yml
Key things to note:
- testing is simpler than
system
because there are no external dependencies - matrix across all supported Rubies and platforms
What Can Go Wrong
In addition to what's enumerated in isolated
's README ...
This strategy works pretty well for simple cases, but looking at the recipe, we can see that the libyaml code is being treated as if it were part of the Ruby C extension that we wrote. That's limiting, because the same compilation step must be shared across the extension code and the third-party library, and we need to be careful about filename collisions between the two filesets.
Maintainers now have an additional responsibility to keep that library up-to-date and secure for your users.