Using PyInstaller (or similar) to create an executable

After following steps posted by @hmasdev and @kevinlinxc, I managed to get it working. Thought it would be worthwhile to post some changes I had to make to get this working, especially with newer versions floating around.

I’m working with Streamlit 1.17.0 (boy have things changed a lot), as well as Pyinstaller 5.7.0. I also use Pipenv, but that shouldn’t really matter here.

  1. Still need to create run_main.py, but we no longer need to create _main_run_clExplicit, as we can call bootstrap.run() directly with the proper flags now.
import streamlit.web.bootstrap

if __name__ == "__main__":
    streamlit.web.bootstrap.run(
        "./main.py",
        "streamlit run",
        [],
        {"_is_running_with_streamlit": True},
    )
  1. Create ./hooks/hook-streamlit.py as shown
  2. Create ./.streamlit/config.toml as shown
  3. To create the spec file, I used pyi-makespec --onefile --additional-hooks-dir=./hooks run_main.py instead of pyinstaller --onefile ..., just because it’s quite a bit faster and we don’t care about the binary compiled by pyinstaller at this point anyways. After that, update run_main.spec as shown.
  4. The final executable can then be compiled using pyinstaller run_main.spec --clean (this avoids the invalid makespec options other people have been seeing)
  5. I agree with @kevinlinxc here, it is much simpler to lift the resultant binary out from ./dist into the working directory to run

Hopefully this helps/updates this a bit!

Edit:

After some tweaking, I managed to get this to compile everything into a true single binary, where you don’t need to keep the executable in the repository for it to run. To do this, I place my streamlit app code (i.e. main.py with any st.write() stuff) into a subdirectory called ./application.

  1. We still need to create run_main.py, but we add in the config we previously set in ./.streamlit/config.toml directly and load it in
import os
import streamlit.web.bootstrap

if __name__ == "__main__":
    os.chdir(os.path.dirname(__file__))

    flag_options = {
        "server.port": 8501,
        "global.developmentMode": False,
    }

    streamlit.web.bootstrap.load_config_options(flag_options=flag_options)
    flag_options["_is_running_with_streamlit"] = True
    streamlit.web.bootstrap.run(
        "./application/main.py",
        "streamlit run",
        [],
        flag_options,
    )

  1. We still must create ./hooks/hook-streamlit.py as previously mentioned
  2. We do not need to create ./.streamlit/config.toml anymore. It shouldn’t hurt if it’s there, but the config will not be used
  3. Create specfile using pyi-makespec --onefile --additional-hooks-dir=./hooks run_main.py (or pyinstaller, I just tend to prefer pyi-makespec) and update it with the following:
datas=[
    (
        "<path to lib>/Lib/site-packages/altair/vegalite/v4/schema/vega-lite-schema.json",
        "./altair/vegalite/v4/schema/"
    ),
    (
        "<path to lib>/Lib/site-packages/streamlit/static",
        "./streamlit/static"
    ),
    (   
        "<path to app code>/application",
        "./application"
    )
],
hiddenimports=[
    "streamlit"
],

The last entry to datas is important, as it tells pyinstaller to bundle in all of the streamlit app code.

  1. Compile the final executable with pyinstaller run_main.spec --clean
  2. The final executable can be run anywhere, no need to move it to any specific location!
6 Likes