1. Is the current version used by the system (e.g. by the dynamic linker) in any situation?
Yes, when using the DYLD_VERSIONED_LIBRARY_PATH
or DYLD_VERSIONED_FRAMEWORK_PATH
environment variables. From man dyld
:
DYLD_VERSIONED_LIBRARY_PATH
This is a colon separated list of directories that contain potential over-
ride 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.
DYLD_VERSIONED_FRAMEWORK_PATH
This is a colon separated list of directories that contain potential over-
ride frameworks. The dynamic linker searches these directories for frame-
works. 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 if the supplied framework is newer. Note:
dyld does not check the framework's Info.plist to find its version. Dyld
only checks the -currrent_version number supplied when the framework was
created.
These variables are only supported for macOS and DriverKit targets.
Additionally, the current_version in the library's Mach-O header can be queried via NSVersionOfRunTimeLibrary()
, and the current_version in the Mach-O header linking against the library with NSVersionOfLinkTimeLibrary()
.
2. When comparing the compatibility version, are all parts of the x.y.z scheme used? Is the comparison lexicographic? Or is there special meaning to x, y and z separately?
All parts are used and the comparison is lexicographic.
Technically, the parts x.y.z form a 32-bit number of the form xxxxyyzz, that is 16-bit x, 8-bit y and z each.
3. Is there documentation on where these version numbers are used? Note that I am asking for where/when they are actually used in practice, not merely for guidelines on how they are recommended to be set.
There's a bit of documentation in man ld
:
-compatibility_version number
Specifies the compatibility version number of the library. When a
library is loaded by dyld, the compatibility version is checked and if
the program's version is greater that the library's version, it is an
error. The format of number is X[.Y[.Z]] where X must be a positive
non-zero number less than or equal to 65535, and .Y and .Z are
optional and if present must be non-negative numbers less than or
equal to 255. If the compatibility version number is not specified,
it has a value of 0 and no checking is done when the library is used.
This option is also called -dylib_compatibility_version for compati-
bility.
But that is only half the truth. For what's really happening, we have to look at dyld sources:
// check found library version is compatible
// <rdar://problem/89200806> 0xFFFFFFFF is wildcard that matches any version
if ( (requiredLibInfo.info.minVersion != 0xFFFFFFFF) && (actualInfo.minVersion < requiredLibInfo.info.minVersion)
&& ((dyld3::MachOFile*)(dependentLib->machHeader()))->enforceCompatVersion() ) {
// record values for possible use by CrashReporter or Finder
dyld::throwf("Incompatible library version: %s requires version %d.%d.%d or later, but %s provides version %d.%d.%d",
this->getShortName(), requiredLibInfo.info.minVersion >> 16, (requiredLibInfo.info.minVersion >> 8) & 0xff, requiredLibInfo.info.minVersion & 0xff,
dependentLib->getShortName(), actualInfo.minVersion >> 16, (actualInfo.minVersion >> 8) & 0xff, actualInfo.minVersion & 0xff);
}
Besides the fact that 0xffffffff
can be used as a wildcard, the interesting bit here is the call to enforceCompatVersion()
:
bool MachOFile::enforceCompatVersion() const
{
__block bool result = true;
forEachSupportedPlatform(^(Platform platform, uint32_t minOS, uint32_t sdk) {
switch ( platform ) {
case Platform::macOS:
if ( minOS >= 0x000A0E00 ) // macOS 10.14
result = false;
break;
case Platform::iOS:
case Platform::tvOS:
case Platform::iOS_simulator:
case Platform::tvOS_simulator:
if ( minOS >= 0x000C0000 ) // iOS 12.0
result = false;
break;
case Platform::watchOS:
case Platform::watchOS_simulator:
if ( minOS >= 0x00050000 ) // watchOS 5.0
result = false;
break;
case Platform::bridgeOS:
if ( minOS >= 0x00030000 ) // bridgeOS 3.0
result = false;
break;
case Platform::driverKit:
case Platform::iOSMac:
result = false;
break;
case Platform::unknown:
break;
}
});
return result;
}
As you can see, if the library declares its minimum supported OS version to be somewhat recent, then the compatibility version is outright ignored by dyld.
So if you rely on the compatibility version being enforced at all, you'll want to use an option like --target=arm64-macos10.13
to build your library.
4. Do any parts of the system have any expectations about how the two types of versions in the metadata should relate to the file name, or to symlinks names created for the library?
Dynamic linking only strictly requires that if your binary asks for a /usr/lib/libz.dylib
, then the library must have exactly that set as its name as well. If the library has an embedded install path of /usr/lib/libz.0.dylib
, then that will be seen as a different library.
However, in the vast majority of cases you will depend on libraries being found on the file system at their install path, and that requires that there be a file at /usr/lib/libz.dylib
that either is the library you're looking for, or a symlink pointing to it. But there's usually no reason to involve symlinks at this stage.
Now, the reason you're seeing versioned file numbers is API breakage. The compatibility_version
field handles forward compatibility: if you link against version 1.2.3
, then any version greater or equal to 1.2.3
will work. But if you ever remove or change the exported API in a way that breaks backwards compatibility, you have to create a new library with a new install name, and keep shipping a copy of the last version of the old library for backwards compatibility.
Symlinks are then used simply for link-time, to point to the most recent version of the library.
Example:
Say you have a /usr/lib/libz.0.dylib
, which had many updates that fixed bugs, expanded the API and bumped the compatibility_version
. All of these will ship as /usr/lib/libz.0.dylib
, and the most recent version of that library will still work with a binary that linked against the very first version of it.
Now you're removing one of the exported functions, and since that is a breaking change, any version from this point out cannot be shipped as /usr/lib/libz.0.dylib
. You thus create /usr/lib/libz.1.dylib
and ship both libs, and dyld will load whatever library the main binary was built against.
But now anyone linking against the library would have to pass either -lz.0
or lz.1
on the command line, which is not particularly nice and requires manual updating, which is generally bad if you want people to adopt the new version. You thus create a symlink from /usr/lib/libz.dylib
to /usr/lib/libz.1.dylib
so that -lz
links against the newest version of the library.