Search code examples
python-3.xpyinstallerpython-poetrysaxon-c

saxonc directly or via saxonpy: how to package into a single windows executable using pyinstaller?


I need to package my python code that uses the saxonpy library into a single executable (for use in Windows platform via the command line). I tried several combinations but the latest attempt is:

(stackoverflow-C7krtO5X-py3.10) PS C:\pythonProject\stackoverflow> pyinstaller --onefile .\main3a.py --name "transfo.exe" --hidden-import nodekind --hidden-import saxonpy

I also tried adding

--add-binary "C:\pythonProject\saxonpy\saxonc_home\libsaxonhec.dll;."

When I run the exe that pyinstaller creates, I get:

PS C:\pythonProject\stackoverflow\dist> .\transfo.exe
Unable to load C:\Users\<username>\AppData\Local\Temp\_MEI257642\saxonpy/saxonc_home\libsaxonhec.dll
Error: : No error

It does work if I set the SAXONC_HOME environment variable to point to a saxonc folder. e.g.:

C:\Users\<username>\AppData\Local\pypoetry\Cache\virtualenvs\stackoverflow-C7krtO5X-py3.10\Lib\site-packages\saxonpy\saxonc_home

probably unnecessary extra info:

Contents of the poetry pyproject.toml file:

[tool.poetry]
name = "stackoverflow"
version = "0.1.0"
description = ""
authors = ["Your Name <[email protected]>"]

[tool.poetry.dependencies]
python = "^3.10"
saxonpy = "^0.0.3"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
import os
import xml.etree.ElementTree as ET
from saxonpy import PySaxonProcessor

def main():
    print('starting code...')
    source_XML = '''
        <cities>
            <country name="Denmark" capital="Copenhagen"/>
            <country name="Germany" capital="Berlin"/>
            <country name="France" capital="Paris"/>
        </cities>
    '''
    parentroot = ET.fromstring(source_XML)
    # has to be unicode for proc.parse_xml()
    xml_str = ET.tostring(parentroot, encoding='unicode', method='xml')

    try:
        with PySaxonProcessor(license=False) as proc:
            proc.set_cwd(os.getcwd())
            xsltproc = proc.new_xslt30_processor()
            xslt30_transformer = xsltproc.compile_stylesheet(stylesheet_file="transformer.xsl")

            xml_doc = proc.parse_xml(xml_text=xml_str)
            # set_initial_match_selection belongs to xslt30_transformer, not xsltproc or proc!
            xslt30_transformer.set_initial_match_selection(xdm_value=xml_doc)
            xslt30_transformer.apply_templates_returning_file(xdm_node=xml_doc,
                                                              output_file="output.xml")

            print("transform complete. file should be created.")
    except Exception as e:
        print(f"exception occured: {e}")

if __name__ == "__main__":
    main()

contents of the transformer.xsl file:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="xml" encoding="UTF-8" indent="yes"/>
    <xsl:template match="/">
        <cities>
            <xsl:for-each select="cities/country">
                <city name="{@capital}" isCapital="true"/>
            </xsl:for-each>
        </cities>
    </xsl:template>
</xsl:stylesheet>

Solution

  • I had a similar issue - my first command line was:

    pyinstaller --hidden-import nodekind --onefile --add-binary="libsaxonhec.dll;." MyScript.py
    

    Then I fixed the directory path:

    pyinstaller --hidden-import nodekind --onefile --add-binary="libsaxonhec.dll;saxonpy\saxonc_home" MyScript.py
    

    The point is that the DLL needs to be put into the expected directory, not just the current directory "."

    However, when I used this command line it revealed a second issue, that the Java VM could not start:

    Excelsior JRE directory "C:\Users\me\Local\Temp\_MEI138362\saxonpy\saxonc_home\rt" not found.
    JNI_CreateJavaVM() failed with result: -1
    

    So, I modified the command line again to include all the binaries from saxonpy:

    pyinstaller --hidden-import nodekind --onefile --add-binary="libsaxonhec.dll;saxonpy\saxonc_home" --collect-binaries saxonpy MyScript.py
    

    Now the Java VM starts OK, but it can't find the timezone mappings:

    can't open C:\Users\me\AppData\Local\Temp\_MEI208802\saxonpy\saxonc_home\rt\lib\tzmappings.
    Error: No styleheet found. Please compile stylsheet before calling transformToString or check exceptions
    

    This particular error may be happening because I am using XSLT, so maybe you won't see this. But anyway, each time I'm getting a bit further :-)

    The final fix was to collect "all", not just the "binaries" for saxonpy:

    pyinstaller --hidden-import nodekind --onefile --add-binary="libsaxonhec.dll;saxonpy\saxonc_home" --collect-all saxonpy MyScript.py
    

    Conclusion

    You have to include a whole bunch of stuff from saxonpy (which makes the EXE quite large - circa 41MB for my simple script) and that has to be directed into the expected path, but it does then work.