Thursday, January 29, 2015

Gettin' fed up with bundlers...

Real Life has been intruding heavily on my coding adventures, but Tuesday and today I got back to trying to solve the issues involved in bundling Cobro, or indeed, any PyQt5/Python3 app.

Here's the status of each.

PyInstaller. I put a shit-ton of time into research on why the bundled app could not find its platform plugin "cocoa". When I finally posted two very detailed notes to the PyInstaller list, another user pointed to a run-time hook (PyInstaller supports run-time hook code that is called by the boot-loader before one's actual program is called) that disables all the five (yes, five) different ways that Qt looks for platform plugins in favor of a completely undocumented folder qt5_plugins. Supposedly PyInstaller should build this automatically and put the relevant plugin DLL into it. It doesn't. When I created it manually, the app started up. Or tried to. It immediately died with a traceback, trying to read the saved settings.

That was an old problem, one I'd struggled with under Nuitka and never solved because it just went away after some change in Nuitka. Now I tackled it again, building a beautifully minimal test case and showing that while the code worked run from the command line, it failed when executed out of a bundle created by either cxfreeze or PyInstaller.

Wrote a highly detailed and awesomely informative post for the PyQt list...

...and sent it to the PyInstaller list.

Fuck. There are few things that can make a person writhe with embarrassment like sending a message meant for one product to the list for another product.

When I realized that the next day, I sent it also to the PyQt list, and next day one user replied with The Answer. Instead of writing

settings.setValue('b',b'\xde\xad\xbe\xef')

instead, use an explicit coercion,

settings.setValue('b', QByteArray(b'\xde\xad\xbe\xef'))

The settings plist that PyQt writes in the two cases is entirely different! And is the same whether the writer is bundled or not, and is accepted by the reader whether bundled or not.

I had supposed PyQt5 would do exactly that coercion automatically, just as it automatically converts between Python strings and QStrings. Apparently not. What it does with raw byte data is (based on my experiences with Nuitka) to use the pickle module. But I don't care; that problem was solved.

So I made that one-line change to Cobro, removed its settings plist and made it write a new one. Then I bundled it with PyInstaller. It still doesn't automatically supply the platform plugin, so I put that in manually; then the "you might be loading two sets of Qt binaries" thing came up; so then I used install_name_tool to manually relativize the links in the platform plugin. (I have become way more familiar with install_name_tool and otool the past weeks than I ever wanted to be.)

Then it couldn't start because QtWebEngine "couldn't find QtWebEngineProcess". WTF? Well, it seems that QtWebEngine requires a "helper" named QtWebEngineProcess which is not only a DLL but a whole goddamn Mac OS app. Documented? I think not! After copying both the DLL and the app folder into the cobro folder, the app still wouldn't start. Now it got into QWebEngine and that died because "Couldn't mmap icudtl.dat".

What?

Well, that's some file that the Chromium browser uses, ghu alone knows what it is or why. I found a copy inside the Chromium app folder in /Applications and copied it into the cobro folder. Now it starts up (but note, if I try to start it up from a different CWD, it again "can't mmap icudtl.dat" so apparently it has to be in the CWD?!?!)

Now the PyInstaller bundled app starts but again on the console is the message "dyld: Library not loaded: @executable_path/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess; Referenced from: /Users/dcortesi/Desktop/scratch/piwork/dist/cobro/QtWebEngineProcess"—and although the app is kinda started, the whole web view is blank and it won't respond to clicks.

Giving up on PyInstaller, I turn to...

cxfreeze. Bundle Cobro afresh. Run it. Much better results: cxfreeze has apparently got the cocoa plugin in the right place and all, there's no QtWebEngineProcess malarky, and the app appears to be running. All the widgets are there; it read the list of comics from settings and is refreshing them; it displays comics. Yeah!

Except... it has no menu bar. It has a dock icon of a generic unix app. If I click that dock icon, then it gets its proper menu bar. When it starts up, no menu bar. Click in another app, then click on Cobro's window, now it has a menu bar.

Well, that's a lot better than PyInstaller but still not acceptable. Which brings us to...

Nuitka: Kay's latest version still doesn't properly relative something, compiling Cobro and running the resulting cobro.exe produces "On Mac OS X, you might be loading two sets of Qt binaries into the same process." and the app comes partway up, but the web engine panel is blank and it has no menu bar.

So, you know... not a good day.

No comments: