TeamPyOgg / PyOgg

Simple OGG Vorbis, Opus and FLAC bindings for Python
The Unlicense
64 stars 27 forks source link

Creating a Wheel for macOS #32

Open mattgwwalker opened 4 years ago

mattgwwalker commented 4 years ago

I've been struggling for most of today to create a PyOgg wheel for macOS.

I cannot work out how to specify a path to ctypes.util.find_library() under macOS. As a consequence, I can't work out how to replicate the current approach (working under Windows) of adding DLLs to the directory containing the Python source files.

I've asked the question on Stack Overflow but if you have any suggestions, I'd love to hear them!

Cheers,

Matthew

mattgwwalker commented 4 years ago

I have a possible solution! Effectively, I've taken the current approach in library_loader.py and prefixed it with a "load internal library" stage.

So using this approach, when a library is to be loaded, we specify the filename. Names for Windows and MacOS would need to be given, plus the general library name if the internal load fails and we attempt to load it externally. I looks like:

    names = {
        "win32": "opus.dll",
    "darwin": "libopus.dylib",
        "external": "opus"
    }
    libopus = Library.load(names, tests = [lambda lib: hasattr(lib, "opus_encoder_get_size")])

And then the library_loader.py file gets an "internal loader" added in:

class Library:
    @staticmethod
    def load(names, paths = None, tests = []):
        lib = InternalLibrary.load(names, tests)
        if lib is None:
            lib = ExternalLibrary.load(names["external"], paths, tests)
    return lib

class InternalLibrary:
    def load(names, tests):
        # Get the name of the library for the current platform                                                                                   
        try:
            name = names[sys.platform]
    except KeyError:
            return None

        # Attempt to load the library from here                                                                                                  
        path = _here + "/" + name
        try:
            lib = ctypes.CDLL(path)
        except OSError:
            return None

        # Check that the library passes the tests                                                                                                
        if tests and all(run_tests(lib, tests)):
            return lib
        return None

What do you think? Would you like this contribution?

mattgwwalker commented 4 years ago

Unfortunately, it appears that this is only a partial solution.

It works for libraries such as ogg and opus. Unfortunately, in its current form it's very much not a solution for the likes of opusfile, which seems to depend internally on ogg, for example.

I'll keep looking into this, but if you have any suggestions, I'm all ears.

mattgwwalker commented 4 years ago

I now have a working solution based on the above technique and the use of delocate, which repairs the internals of the dylibs that PyOgg requires (such that the system loads them from the PyOgg library and doesn't search for them in the standard system locations). I've tested it on OSX 10.13 and 10.15.

@Zuzu-Typ I'm not sure what the best approach is for sharing/documenting the process. Do you want to store the dylibs in the git repository? I can't see the DLLs stored there, so perhaps you'd prefer a different approach.

mattgwwalker commented 4 years ago

The technique even works when PyOgg is bundled by PyInstaller.

PyInstaller, however, doesn't automatically find the libraries (for that we'd have to go to string literals inside the ctypes calls, and I think that'd be a Bad Idea). But if the dylibs are specifically passed in using --add-binary and the dylibs are located inside the default directory ('.'), then all goes well.

mattgwwalker commented 4 years ago

Ha! Better yet, PyInstaller has a library of hooks that it uses to know what to do with specific Python distributions, and they're open to submissions. The hook needed for PyOgg is all of two lines. Then everything would just work whenever a user tried bundling their PyOgg-using application with PyInstaller.

The hook file is just these lines:

from PyInstaller.utils.hooks import collect_dynamic_libs

binaries = collect_dynamic_libs("pyogg")
mattgwwalker commented 4 years ago

The continuous integration tests are working under macOS using the pre-compiled binaries that are now included in the git repository. In order to tick off this issue we need to: