Monday, February 23, 2015

Giving up on pyqtdeploy; breakthrough with PyInstaller

My very helpful correspondent Lloyd built Cobro on his system with pyqtdeploy and it ran. I have what appears to be the same setup, but when I build it, it doesn't. After days of struggle, I wrote pyqtdeploy off and turned back to PyInstaller.

PyInstaller is only a beta under Python3. More than a year ago, I contributed quite a bit of time and code to making it work, not only with Python3 but using ModuleGraph as its analysis tool in place of its previous method. My code was integrated but the Python3 branch got only some attention from the primary developers. Also relevant here: PyInstaller uses a system of "hooks" for module-specific work. There are a number of PyQt-related hooks. A hook receives as its argument a "mod" object, which was formerly a Module instance as created by the old import analyzer. The hook could add or delete binary dependencies from the mod and do other things to it.

When I changed the code to use ModuleGraph, that interface had to change. There was no more Module object. But rather than change all the hooks, I crafted a FakeModule class. Before calling a hook, PyInstaller makes a FakeModule and passes it to the hook. The fake mod argument can be interrogated the way the old mod could be.

Unfortunately the most common thing a hook does to its mod is to add or delete some dependencies by use of an expression like

    mod.binaries.append( list-of-descriptive-tuples )

Unfortunately the binaries member of a fake module had to remain read-only. The hook could examine it to find out if some binary was already included. But to add or delete binaries, the hook now has to have

    mod.add_binaries( list-of-descriptive-tuples )

When my changes were merged into the Python3 branch, all of the then-existing hooks were updated to use this API.

When a few weeks ago, I tried PyInstaller for Cobro, the resulting Mac OS app would immediately crash for lack of a platform plug-in. Eventually some hours of effort revealed that the PyQt5 hooks were putting the platform plugins in a nonstandard place, and playing a naughty game with the QApplication to make that happen. But at least for other people, the platform plugins did get put in that place. For me, they weren't.

So now I went back to PyInstaller and examined more carefully what was going on. Gradually the work I'd done last year came back to me and in tracing through the code, I discovered that the set of PyQt5 hooks were not using my Python3, Modulegraph, API. They were probably created when Qt5 first came out, which was after I'd stopped work on it. They were probably just copied from the current version of the PyQt4 hooks, and merged into the Python3 branch unchanged.

So all the PyQt5 hooks were trying to do mod.binaries.append(), which had absolutely no effect on the final bundle. All I had to do was update them to use the correct API and bingo, it copied the plugin files to where they should be. So I cloned the Python3 branch and made those changes and issued a pull request.

Cobro still didn't work as bundled by PyInstaller, for a reason I'll cover in the next post. But here's the big news: PPQT2 does work! PyInstaller, under Python3, with the changes I just made, correctly bundles PPQT2 as a Mac OS app bundle! This suggests that in the not too-distant future, after I've installed all the needful bits into my Ubuntu and Win7 VMs, I'll be able to roll out a PPQT2 alpha.

That will have to wait at least 2 more weeks. Thursday we head off for a ten-day run up to Portland and Seattle. But not long after I get back, hopefully before the end of March, I'll be able to push out that alpha. In my spare time on this trip I plan to start work on the Translator interface, which is the one remaining feature of PPQT2 that is not coded.

No comments: