Using nuitka compiler for python3 on Alpine Linux ARMHF (musl)
Motivation
pidoctor is written in Python (as there was no easy way to get Crystal to work on ARMHF / musl).
This means a dependency on Python – which adds overhead.
I suspect that this overhead is the reason that pidoctor will not run on 256 MB Raspberry Pi’s – because the RAM is exhausted by all the packages which are required.
Furthermore, it is desirable to have the distribution be as small a download as possible! This means Python has to be removed from the equation somehow, the package should produce a binary (as would be the way with Crystal).
Luckily, there is nuitka.
And – lucky you – I have worked out some kinks of using nuitka on Alpine Linux, ARMHF (maybe it will work for x86 too).
Nuitka
nuitka is a fantastic piece of software, which will take a Python script and compile it to an executable, thereby speeding it up dramatically.
nuitka does all the heavy lifting, you don’t even need to adjust anything in your source code (methinks). You can run it like this:
python3 -m nuitka –follow-imports –standalone pidoctor.py
Note the –standalone flag, which will compile the necessary shared libraries into your <projectname>.dist folder – thereby the installation will be able to run without any dependency on Python!
Please note: WordPress unfortunately fucks up the double dashes and makes them into single dashes. the –m has a single dash, the –follow-imports and the –standalone have double dashes.
Installation of Nuitka on Alpine Linux (specifically arhmf, might work on other targets)
apk add python3-dev
apk add chrpath
pip3 install –U nuitka
optionally, if you are running in diskless mode (to add nuitka to the files which should be persisted):
lbu add /usr/bin/nuitka3-run
lbu add /usr/bin/nuitka3
lbu add /usr/lib/python3.6/site-packages/nuitka
lbu commit –d
Note: nuitka’s source is in Python.
Check that nuitka runs:
python3 –m nuitka
should execute without showing any syntax errors.
Fixing nuitka to run with Alpine Linux armhf
Important: WordPress unfortunately badly mangles the syntax. Please refer to the GitHUB issue I opened, where I also attached the patch files for a reference.
nuitka calls several support binary utilities on your system, and expects them to return the results in a certain fashion. This is, where nuitka initially stumbles.
First error
pidoctor:/opt/pidoctor# python3 -m nuitka –follow-imports –standalone pidoctor.py
Traceback (most recent call last):
File “/usr/lib/python3.6/site-packages/nuitka/__main__.py”, line 188, in <module>
main()
File “/usr/lib/python3.6/site-packages/nuitka/__main__.py”, line 182, in main
MainControl.main()
File “/usr/lib/python3.6/site-packages/nuitka/MainControl.py”, line 846, in main
Plugins.considerExtraDlls(dist_dir, module)
File “/usr/lib/python3.6/site-packages/nuitka/plugins/Plugins.py”, line 102, in considerExtraDlls
for extra_dll in plugin.considerExtraDlls(dist_dir, module):
File “/usr/lib/python3.6/site-packages/nuitka/plugins/standard/ImplicitImports.py”, line 372, in considerExtraDlls
uuid_dll_path = locateDLL(“uuid”)
File “/usr/lib/python3.6/site-packages/nuitka/utils/SharedLibraries.py”, line 71, in locateDLL
return dll_map[dll_name]
KeyError: ‘libuuid.so.1.3.0’
Reason behind the error: nuitka calls ldconfig –p and expects it to list a list of cached shared libraries, in a format like this:
libICE.so.6 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libICE.so.6
on Alpine Linux, ldconfig –p fails – as there is no such option. ldconfig on Alpine Linux is incapable of producing a list of the form which Nuitka wants.
Fix:
in /usr/lib/python3.6/site-packages/nuitka/utils/SharedLibraries.py
add the following code immediately after the import statements at the top:
def find_alpine(name,paths):
for path in paths:
for root, dirs, files in os.walk(path):
if name in files:
return os.path.join(root,name)
and in locateDLL, before import subprocess, add the following code:
if os.path.isfile(‘/etc/alpine-release’):
return find_alpine(dll_name,[“/lib”,”/usr/lib”,”/usr/local/lib”])
else:
import subprocess(…)
The (…) above indicates that the remainder of the code of locateDLL should be indented under the else.
What this code does: if the file /etc/alpine-release exists – indicating that we are running on Alpine Linux, we search for the shared library in three predefined directories:
- /lib
- /usr/lib
- /usr/local/lib
and return the first matching file. Note, we do not handle the situation in case the file is not found – in this case nothing is returned.
Second error
Traceback (most recent call last):
File “/usr/lib/python3.6/site-packages/nuitka/__main__.py”, line 188, in <module>
main()
File “/usr/lib/python3.6/site-packages/nuitka/__main__.py”, line 182, in main
MainControl.main()
File “/usr/lib/python3.6/site-packages/nuitka/MainControl.py”, line 859, in main
standalone_entry_points = standalone_entry_points
File “/usr/lib/python3.6/site-packages/nuitka/freezer/Standalone.py”, line 1169, in copyUsedDLLs
used_dlls = detectUsedDLLs(source_dir, standalone_entry_points)
File “/usr/lib/python3.6/site-packages/nuitka/freezer/Standalone.py”, line 1062, in detectUsedDLLs
assert os.path.isabs(dll_filename), dll_filename
AssertionError: ldd
Reason behind the error: nuitka calls ldd with each shared library (“dll_filename”) to be added to your .dist folder. The purpose of this is to find the dependencies of these shared libraries themselves. (A recursive search if you will).
For example:
pidoctor:/opt/pidoctor# ldd /usr/lib/libexpat.so.1
ldd (0x76f43000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x76efd000)
libc.musl-armhf.so.1 => ldd (0x76f43000)
As you see, libexpat.so.1 has two dependencies:
- libgcc_s.so.1 –> to be found at /usr/lib/libgcc_s.so.1
- libc.musl-armhf.so.1 –> to be found at ldd
and this is precisely where nuitka has problems. It wants to copy a file with an absolute path, and is given the name “ldd” instead. Again, something which is specific to Alpine Linux.
Therefore the assertion fails, and nuitka does not continue.
Here’s how to find out which file it actually references:
pidoctor:~# which ldd
/usr/bin/lddpidoctor:~# ls -alh /usr/bin/ldd
lrwxrwxrwx 1 root root 28 Jan 1 1970 /usr/bin/ldd -> ../../lib/ld-musl-armhf.so.1
Note that ../../lib/ld-musl-armhf.so.1 is a relative path, which actually refers to /lib/ld-musl-armhf.so.1 on my system. And on yours, probably, too.
Fix:
search for libutil.so. in the file /usr/lib/python3.6/site-packages/nuitka/freezer/Standalone.py
There is a line with result.add(filename) below it. Just above this line, insert:
if filename == ‘ldd’:
filename = os.path.join(os.path.dirname(‘/usr/bin/ldd’),os.readlink(‘/usr/bin/ldd’)) #e.g. ‘/lib/ld-musl-armhf.so.1’ fix for Alpine
Third error
Error, needs ‘chrpath’ on your system, due to ‘RPATH’ settings in used shared
Fix:
Did you install chrpath as advised above?
apk add chrpath
That’s it – now nuitka should perform it’s magic without hiccups. I will send the author of nuitka the patches and this blog article, maybe he can include them in nuitka.
Factory version
Update 2.2.2019: The author of Nuitka added my patches (in a slightly modified form). In the current “factory” version, the –standalone compilation works for me. Please refer to this github page for details: