-
Notifications
You must be signed in to change notification settings - Fork 145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
rbthemis: Extra effort to locate Themis Core library #917
Conversation
This one is a cousin of #915 for Ruby. As I've learned, Libraries loaded at runtime are expected to follow the rules for libraries loaded on startup. That is, use absolute installation paths. While on Linux you can normally load libraries by their stem name (e.g., With Python setting Also, the paths searched here on macOS are from Homebrew's "cellar". That is an attempt to make things work if you do
As this does work for compiled software. I'm not sure it's a good idea though. It certainly makes releasing things a bit harder, adding one more place with a version to be bumped. |
@@ -32,7 +32,54 @@ def empty?(value) | |||
|
|||
module ThemisImport | |||
extend FFI::Library | |||
ffi_lib 'themis' | |||
|
|||
THEMIS_CORE_VERSION = '0.14.0' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm, so for every release it will be +1 place where we should update version in addition to src/wrappers/themis/ruby/rbthemis.gemspec
? or it should use old version of themis, yes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm, so for every release it will be +1 place where we should update version in addition to src/wrappers/themis/ruby/rbthemis.gemspec ?
Correct. That's one more place to update. Which is why I don't particularly like making this change.
But it seems to be the only way to support brew unlink
use case. For compiled applications, the linker does this step of "resolve /usr/local/lib/libthemis.dylib
symlink and remember the install path from the library metadata, which includes the version".
Keep in mind, this the version of Themis Core formula, not the version of rbthemis
gem.
or it should use old version of themis, yes?
If this constant is not updated, most likely the fallback will work selecting some version of Themis Core, which is still likely to work. It will get hairy if the system has multiple versions of Themis Core installed, but normally Homebrew is really against that.
One option, actually, would be to hardcode this path:
/usr/local/opt/libthemis/lib/libthemis.0.dylib
which I think should point to the latest version of libthemis
installed in the system.
I guess this will be /opt/homebrew/opt/libthemis/lib/libthemis.0.dylib
on M1, but I have no way to check...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep in mind, this the version of Themis Core formula, not the version of rbthemis gem.
hm, I thought that it is gem's version because where homebrew can get version of themis when install ruby gem... Now I don't understand how it works and how it actually installs *(
Can you explain briefly how homebrew install gems on MacOS and where it gets /opt/homebrew/Cellar/libthemis/#{THEMIS_CORE_VERSION}/lib/libthemis.0.dylib
this path. Does the binary library have version in the metadata?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/opt/homebrew/opt/libthemis/lib/libthemis.0.dylib
looks like you're right and that's the path on M1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add this file to release checklist so we won't forget to update it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lagovas, your confusion is exactly what Themis users experience when the docs tell them that in addition to Ruby library they need to install some "core" library as well. And I expect most of them could care less about this split and don't really want to know the intricacies.
So, "gems" are Ruby libraries, installed by gem
. They contain Ruby code. rbthemis
is the one for RbThemis.
Then, rbthemis
needs Themis Core dynamic library to actually function. That one is packaged for macOS via Homebrew, installed with brew
. Homebrew does not (normally) install Ruby gems. The libthemis
package for Homebrew contains only native libraries for Themis Core.
Then Homebrew installs the following:
/opt/homebrew/Cellar/libthemis/${libthemis_version}
– this is the "real" prefix oflibthemis
installation- there could be multiple versions installed:
.../libthemis/0.14.0
,.../libthemis/0.15.0
, etc. - the version here is the version of Homebrew formula
include
subdirectory contains C headerslib
subdirectory contains libraries:.../lib/libthemis.0.dylib
is the "real" library.../lib/libthemis.dylib
is a symlink tolibthemis.0.dylib
- there could be multiple versions installed:
/opt/homebrew/opt/libthemis
– convenience symlink to../Cellar/libthemis/${latest_libthemis_version}
- this symlink is updated every time
libthemis
is updated and a newer version is installed - that's a symlink to the entire directory
- this symlink is updated every time
/opt/homebrew/include/themis/themis.h
, .../opt/homebrew/lib/libthemis.dylib
,libthemis.0.dylib
, ...- when a formula is "linked", Homebrew also installs symlinks to every individual file in "standard" locations
On Intel machines s=/opt/homebrew=/usr/local=g
, that's a different Homebrew prefix.
- The
Cellar
directory is the "canonical" place where formulas are installed. - The
opt
directory is version-agonstic place. - The "standard" directories contain symlinks for the benefit of user.
When you configure your C compiler, you add /opt/homebrew/include
to header search paths, /opt/homebrew/lib
to library search paths. Then the compiler is able to find any "linked" library installed via Homebrew. (On Intel machines /usr/local/include
and /usr/local/lib
are usually already in the default search paths.)
This is why Homebrew installs these symlinks normally. It is possible to "unlink" a library if it conflicts with other installation. Homebrew calls this "keg-only" installation. This is usually used to allow the system library to be used by default when building software – but you configure your compiler to look into /opt/homebrew/opt/libthemis/include
for applications that build with Themis, and it still works.
Another use case for "keg-only" installations is experimental new versions. For example, we could publish libthemis@2
which you could install in parallel with normal libthemis
and experiment with it. Later libthemis@0
could be installable as "keg-only" to allow software built against Themis 0.x to still work, while libthemis
could be linked to build new software with it.
So, you configured your compiler. Now, when you write -lthemis
in your C flags, the compiler takes themis
stem, slaps lib...
and ...dylib
to it, then looks for libthemis.dylib
in the search paths. Normally it finds:
/opt/homebrew/lib/libthemis.dylib
which is a symlink to
/opt/homebrew/lib/libthemis.0.dylib
which is a symlink to
/opt/homebrew/Cellar/libthemis/0.14.0/libthemis.0.dylib
which is the "real" name of the file that gets linked.
When the linker links the executable, it records the "install name" of the library into the executable. This is the canonical absolute path of the library recorded in its metadata. When we compile Themis Core, we set that to the "real" path where Homebrew would install the library.
$ otool -l /usr/local/lib/libthemis.dylib
[...]
Load command 4
cmd LC_ID_DYLIB
cmdsize 72
name /usr/local/opt/libthemis/lib/libthemis.0.dylib (offset 24)
time stamp 1640176814 Wed Dec 22 21:40:14 2021
current version 0.0.0
compatibility version 0.0.0
[...]
This is the path which gets recorded in the compiled binary, this is the path which the dynamic library loader will look for when the application is launched.
On Linux the linker usually records only the "stem", the library soname like libthemis.so.0
. Then at load time the loader looks for the library in whatever search paths it has. On macOS it's either an absolute path, or certain limited relative paths like @executable_path/...
relative the binary which is loaded.
Now enter Ruby (and Python) which don't use "normal" dynamic loader. Instead the load the library dynamically by calling dlopen()
. Since there is no prior compilation stage, they don't care for the "install name" of the library.
When you do
ffi_lib 'themis'
it expands the name to libthemis.dylib
and then looks for it in a bunch of hardcoded paths:
whichever loads first – that's your library.
It happens to work for us, but if you want to be more strict – to behave like a compiled application would do – you'd better check the "install name" of the library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To sum it up, I believe the correct list to try should be
'/usr/local/lib/libthemis.0.dylib', # make install puts it there
'/opt/homebrew/opt/libthemis/lib/libthemis.0.dylib', # try M1 first
'/usr/local/opt/libthemis/lib/libthemis.0.dylib', # try x86, even on M1 (if running emulated)
'libthemis.0.dylib', # uh... non-Homebrew install?
'themis', # final attempt
That way we'd visit all the usual places where it could be found, and whatever version is installed with make install
takes priority.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for such great explanation
but main my question was where we declare version for homebrew)) because i'm not familiar with that and don't know where it gets ${libthemis_version}
. You mentioned Formula
, so I understood that there should be some manifest file for homebrew and we somehow deploy it into some repository. Went to our buildbot and found that it looks on https://github.com/cossacklabs/homebrew-tap/blob/master/Formula/libthemis.rb#L7.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but main my question was where we declare version for homebrew))
*shrinks in embarrassment*
I understood that there should be some manifest file for homebrew and we somehow deploy it into some repository
You're absolutely right, that's the place which defines the version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworked the path lists. Also tested more thoroughly on Linux with RVM-provided Ruby and decided to just ignore absolute paths on Linux (except for /usr/local
override) since only distribution-provided Rubies seem to know about the "correct" paths where distribution package manager would install libthemis
.
@@ -32,7 +32,54 @@ def empty?(value) | |||
|
|||
module ThemisImport | |||
extend FFI::Library | |||
ffi_lib 'themis' | |||
|
|||
THEMIS_CORE_VERSION = '0.14.0' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add this file to release checklist so we won't forget to update it
Just like with PyThemis, migrate RbThemis to the same pattern of loading Themis Core library dynamically with more attention to versioning. Detect the platform and load properly versioned libraries first, reducing the chance for an ABI mismatch. However, keep the 'themis' as a fallback (and a default action for Windows). Ruby's "ffi_lib" is in some ways more smart than Python's find_library (it supports Apple M1 by accident, for example) but in some ways it's also dumber. Don't depend on ffi's maintainers and hardcode a list of absolute paths to try first in attempt to make things "just work".
Just like with PyThemis, migrate RbThemis to the same pattern of loading Themis Core library dynamically with more attention to versioning.
Detect the platform and load properly versioned libraries first, reducing the chance for an ABI mismatch. However, keep the
'themis'
as a fallback (and a default action for Windows).Ruby's
ffi_lib
is in some ways more smart than Python'sfind_library
(it supports Apple M1 by accident, for example) but in some ways it's also dumber. Don't depend on ffi's maintainers and hardcode a list of absolute paths to try first in attempt to make things "just work".Checklist