I was looking at versioning a shared library for a small personal project when the docs for the SOVERSION
target property mentioned that on Mach-O systems such as OS X and iOS, it corresponds to the "compatibility version", while VERSION
corresponds to the "current version".
On Linux, it's simply something like VERSION 5.4.2
and SOVERSION 5
to indicate that the library is compatible with version 5.0 and newer, and VERSION
is used as the DLL image version in the form <major>.<minor>
on Windows (I'm not sure what difference SOVERSION
makes on Windows).
However, the example in the docs for the referenced FRAMEWORK
target property illustrate how you might have VERSION 16.4.0
and SOVERSION 1.0.0
on Mach-O platforms (I'm not interested in building a framework, just wondering about the foreign versioning scheme.)
Just how does versioning work in the Mach-O world? I'm used to just bumping the major version if I remove some functionality, which would be a compatibility break, so how is it possible that a library at version 16.4.0 remains compatible with the 1.0.0 version of the library? What does "compatible" mean?
First off, just to get this out of the way, frameworks are just dylibs that are named Something.framework/Something
rather than libsomething.dylib
. The file format is exactly the same though, so throughout this post I'll be simply referring to them as dylibs.
Now, let's start with an excerpt from the mach-o/loader.h
header (the de-facto authoritative source for the Mach-O file format):
/*
* Dynamicly linked shared libraries are identified by two things. The
* pathname (the name of the library as found for execution), and the
* compatibility version number. The pathname must match and the compatibility
* number in the user of the library must be greater than or equal to the
* library being used. The time stamp is used to record the time a library was
* built and copied into user so it can be use to determined if the library used
* at runtime is exactly the same as used to built the program.
*/
struct dylib {
union lc_str name; /* library's path name */
uint32_t timestamp; /* library's build time stamp */
uint32_t current_version; /* library's current version number */
uint32_t compatibility_version; /* library's compatibility vers number*/
};
/*
* A dynamically linked shared library (filetype == MH_DYLIB in the mach header)
* contains a dylib_command (cmd == LC_ID_DYLIB) to identify the library.
* An object that uses a dynamically linked shared library also contains a
* dylib_command (cmd == LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB, or
* LC_REEXPORT_DYLIB) for each library it uses.
*/
struct dylib_command {
uint32_t cmd; /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
LC_REEXPORT_DYLIB */
uint32_t cmdsize; /* includes pathname string */
struct dylib dylib; /* the library identification */
};
As explained in the comments, a struct dylib
is embedded in both the library as well as in the binary linking against it, both containing a copy of current_version
and compatibility_version
. How the latter works is explained right there, but the former is not addresses.
Documentation for that can be found on the dyld
man page (source is here, but not pretty to look at outside of man
):
DYLD_VERSIONED_FRAMEWORK_PATH
This is a colon separated list of directories that contain potential override frame-
works. The dynamic linker searches these directories for frameworks. For each
framework found dyld looks at its LC_ID_DYLIB and gets the current_version and
install name. Dyld then looks for the framework at the install name path. Whichever
has the larger current_version value will be used in the process whenever a framework
with that install name is required. This is similar to DYLD_FRAMEWORK_PATH except
instead of always overriding, it only overrides is the supplied framework is newer.
Note: dyld does not check the framework's Info.plist to find its version. Dyld only
checks the -current_version number supplied when the framework was created.
[...]
DYLD_VERSIONED_LIBRARY_PATH
This is a colon separated list of directories that contain potential override
libraries. The dynamic linker searches these directories for dynamic libraries. For
each library found dyld looks at its LC_ID_DYLIB and gets the current_version and
install name. Dyld then looks for the library at the install name path. Whichever
has the larger current_version value will be used in the process whenever a dylib
with that install name is required. This is similar to DYLD_LIBRARY_PATH except
instead of always overriding, it only overrides is the supplied library is newer.
So in short:
compatibility_version
is used to determine whether a library is "new enough" for a binary that wants to load it.current_version
is used to pick a library when more than one are available.As for your confusion about having a current version of 16.4.0
with a compatibility version of 1.0.0
: from looking at some sources, Apple seems to bump the major version whenever there is any kind of feature introduced, and use the minor version(s) for pretty much only bug fixes, AFAIK.
So what they call 16.4.0
, I'd probably call 1.16.4
. ;)