What I would otherwise forget
Feb 2018
Problem: make the Ruby C API visible to Swift using SPM in a way that is useful for other users.
(From a place of macOS by default with Linux as an exception…)
Ruby can be installed in lots of different places.
I happen to have four versions installed under rbenv
and one system version
from Apple.
My RBENV_ROOT
(although that env var is not set) is ~/.rbenv
. Other
people on the internet have a system-wide installation under /usr/local
.
Other Ruby version managers exit. Presumably these do not install into paths
involving rbenv
.
On Linux & macOS Homebrew, the headers end up in prefix/ruby-x.y.z
. Debian
puts the arch header directory under in prefix/arch/ruby-x.y.z
.
All this means is that the Ruby umbrella header file and include directories can be in various places depending on how the user’s machine is set up and what version of Ruby they want to run against.
For the system Ruby, the header files are shipped as part of Xcode rather than
macOS, in $(xcrun --show-sdk-path --sdk macosx)/System/Library/Frameworks/Ruby.framework/Headers
. Apple have munged the arch-specific include dir
(ruby/config.h
) into the main one.
And that xcrun
could give different answers on different machines.
On Linux, #include <ruby.h>
fails to compile due to a missing dependency on
<termios.h>
for pid_t
or something. Maybe this is due a difference between
clang
used by SPM and gcc
that Ruby probably expects. Easy enough to
work around.
When a header is given in a module map, and that header includes others, Clang is smart enough to look for them relative to that first header. The native macOS Ruby structure is:
include/ruby-r.m.f
ruby.h
ruby/
ruby.h
<other stuff>
x86_64-darwin17/ <or whatever>
ruby/
config.h
The module map contains the higher-up ruby.h
. This means all of its includes
of the form #include <ruby/foo.h>
get resolved by clang no problems. But it
also does #include <ruby/config.h>
which will not get resolved without passing
an explicit -I
option.
Apple’s distribution puts config.h
into the regular directory which means no
-I
is needed once the module map points to the right ruby.h
.
The system Ruby is shipped as /usr/lib/libruby.2.3.0.dylib
with regular
symlinks so -lruby
works. otool -L
tells us this depends in turn on
/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/libruby.2.3.0.dylib
which depends on Core Foundation.
The ruby
s built by default with rbenv install
provide only a static library.
My earlier rbenv
installations have called this libruby-static.a
; latest
rbenv
has adopted a less risky naming scheme of libruby.2.5.0-static.a
.
(This ‘no shared lib’ thing is a 6.5 year-old open issue in ruby-build which provides the command to install a version with the shared library.)
Trial and error reveals that the static library depends on Foundation
(which
is reassuring given the dylib does.)
So this is a step more complicated than the headers: not only the location but also the name of the library + its link options can vary from machine to machine.
Installing via macOS Homebrew leaves a libruby.dylib
as well as the expected
symlinks in prefix/lib
. Linux -dev
packages do not do this, they leave
just the expected versioned symlinks. Debian packages put the libs in
/usr/lib/x86_64-linux-gnu
.
When linking with a static library, clang
produces warnings if any of the
object files were compiled with a different target than the current setting.
SwiftPM defaults to a target of something-something-10.10. But rbenv install
defaults to “whatever the current macOS is” - meaning, for latest Ruby 2.5,
something-something-10.13 (High Sierra).
To avoid the warnings - let’s assume they’re there for a reason - we have to tell SwiftPM to use the more recent target.
Linking against this static library also produces the heart-stopping:
(kernel) Failed to query kext info (MAC policy error 0x1).
Failed to read loaded kext info from kernel - (libkern/kext) internal error.
Luckily swift build
still has exit code 0 and seems to work fine. So we’ll
ignore that.
So, pkg-config
to the rescue? Well, sort of: Ruby from rbenv
etc. does
provide a .pc
file. However it is stored alongside that version of Ruby and
not added to $PKG_CONFIG_PATH
.
Once found, pkg-config --cflags
works but pkg-config --libs
is a bit
broken: it doesn’t support --static
properly and if the Ruby installation
doesn’t have a dylib then it doesn’t bother to mention a Ruby library at all.
Ruby from rvm
provides and does not install an amusingly broken
pkg-config --libs
. Linux ruby-dev
and macOS Homebrew packages properly
install a working pkgconfig file.
Wrapping an existing library requires an entire package, can’t just be a target of something else.
The module map has to contain the absolute location of the umbrella header, and the name of the library to link with.
Ideally we would set compiler flags here. SwiftPM does not allow this. It does allow a pkgconfig filename to be given which SwiftPM itself will parse and use to find compiler/linker flags. However if any of these flags are not on a “whitelist” (basically -I / -F / -L/l) then the are all thrown away and the pc file is ignored. This makes the feature almost entirely useless. I can imagine the line of discussion that led here but still…
SwiftPM has a complex list of places to look for .pc files. None of these include anything useful like ‘the package root directory’ or ‘the current directory’.
Clang default lib paths, from clang -v -Xlinker
are (today):
Now, /usr/local/lib
is where homebrew puts its stuff by default, so there
is definite chance of a mixup here: doing clang -lruby
will always grab the
/usr/local/lib
version.
But be careful, swiftc
is not clang
. Libraries mentioned in module maps
passed to swiftc -fmodule-map-file
do not appear to resolve in /usr/local
ahead of the SDK – observation not proof, mind.
Choices seem to be:
/usr/lib
, headers in a single directory inside
Xcode.prefix/lib
, headers in
prefix/include/ruby-2.X.Y
. Prefix may be more complicated on Linux with
separate arch path part.Case (2) may be symlinks from a case (3) install but regular users will not know the origin.
Linux package install & macOS Homebrew actually both provide and install a
decent pkgconfig
file. Nothing else does. But, due to harmless linker flags
SwiftPM will refuse to use these pkgconfig
files. Because native Ruby splits
its include files over two directories, the only ‘just works’ option is to
target the macOS system default Ruby with the single-directory Xcode headers.
All other configs + platforms will need at minimum -Xcc -I
passed to deal with
the ruby/config.h
directory.
So we will:
swift package edit
session that
will update the various files for a Ruby of the user’s choice including
creation of a package-config file if necessary to wrap up the compile + link
arguments.This will have to be done for all down-stream consumers, eg. if TMLRuby
is
a library package that uses CRuby
and TopazFancy
is an executable package
using TMLRuby
then building TopazFancy
will require this Ruby config
bootstrap step to use a non-system default Ruby (or work at all on Linux).
Never thought I would miss autoconf.