Usage of current version and compatibility version on macOS
Asked Answered
B

1

5

On macOS, a dylib has a compatibility version and a current version as metadata, each having a form of x.y.z.. otool -L can display these.

How and when are these versions used by the system, or by various tools/program?

For the compatibility version, I was able to find that:

  • When linking a program against a library, the compatibility version of that library will be recorded into the program. The dynamic linker will refuse to link to a library whose compatibility version is smaller than what was recorded. (reference, although this was for frameworks).

Questions:

  • Is the current version used by the system (e.g. by the dynamic linker) in any situation?
  • 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?
  • 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.
  • 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? E.g. with ls -l /usr/lib/libz* I see many different symlinks for the same library, each with a different version in the name.
Blasphemy answered 12/4, 2021 at 9:16 Comment(0)
C
8

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.

Cologarithm answered 12/4, 2021 at 23:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.