Using PyInstaller (or similar) to create an executable

Wow, great detective work @s_mc!

I think there’s some work Streamlit devs can do to make this easier in the future, but for now it sounds like all that’s left for you is to turn global.developmentMode off — which happens to be quite simple :smiley:

Here are a few ways to do it:

  • Add the code below to either $HOME/.streamlit/config.toml or .streamlit/config.toml (in the folder you’re running Streamlit from)

    [global]
    developmentMode = false
    
  • Pass --global.developmentMode=false to the streamlit run command

  • Set the STREAMLIT_GLOBAL_DEVELOPMENT_MODE environment variable to false

Let me know how it goes!

Team, any luck here? I’m really waiting for an easy pyinstaller functionality for streamlit!

I didn’t pursue this further, my solution was to run my streamlit program on a VM. When the VM boots it starts a service that runs my streamlit program. I would still prefer a pyinstaller compatible version of streamlit though.

1 Like

The StreamlitWrapper script did not start streamlit for the current version. Also, Credentials object now has ._check_activated (instead of .check_activated) attribute. Running:
python streamlitWrapper.py did not start streamlit, the script ran without throwing any exception but nothing happened.
Really looking forward to the Teams version which will allow packaging of streamlit app into single executable file for distribution - I know there’s a separate thread about bundling Streamlit + Electron as well here. Fingers crossed either of these will be implemented by Streamlit dev.

Commenting out the line “if version.should_show_new_version_notice():” in _main_run_clExplicit seems to make it work.

can anyone sum up the entire process if you have figured it out? Thanks!

1 Like

By having this function run as the main function of the compiled script, I’m able to get streamlit to try to start the webapp under pyinstaller.

def streamlit_run():
    this_dir = Path(__file__).parent
    os.chdir(this_dir)
    sys.path.append(this_dir.name)
    sys.argv = ["streamlit", "run", "app.py", "--global.developmentMode=false"]
    sys.exit(stcli.main())

I changed the spec file (from pyinstaller) so that it could find the import for app.py (by specifying to copy over the script).

Although streamlit starts up the local server (usual output with You can view your Streamlit app... and on port 8501), it gives a 404: Not Found error on actually loading the webpage.
This is as far as I’ve managed to get.

Hi david,

Can you explain how this is done. Maybe include your code in larger example. How are you aming use of this function that you have created?

[Note This still doesn’t work]

I created an example project like so:

streamlit-build-test/
    - env.yml
    - examply.py
    - hook-streamlit.py
    - streamlit_run.py

The env.yml is for the conda environment: [I also checked with a virtualenv environment, and had the exact same problem]

name: streamlit-build-test
channels:
  - defaults
dependencies:
  - python=3.6
  - pip
  - pip:
      - streamlit

example.py is:

import streamlit as st
if __name__ == '__main__':
    x = st.text('foo')

Pyinstaller couldn’t find streamlit on its own, so I had to add hook-streamlit.py:

from PyInstaller.utils.hooks import copy_metadata
datas = copy_metadata('streamlit')

And then streamlit_run.py is a wrapper for the app, that essentially calls streamlit run example.py:

import sys
import streamlit.cli as stcli

if __name__ == '__main__':
    sys.argv = ["streamlit", "run", "example.py", "--global.developmentMode=false"]
    sys.exit(stcli.main())

Now, to compile the app, run:

$ pyinstaller --onefile --additional-hooks-dir=. -w streamlit_run.py

Then run the compiled app with:

$ dist/streamlit_run

The command line shows streamlit is running, but unfortunately the browser can’t seem to find the page (at the ip) on localhost, and gives a 404 not found error.

1 Like

Hello! I’ve been going through this forum looking for ways to share my streamlit app with colleagues. I have it on a virtual machine right now but I’m stuck on how to share the link from the virtual machine to non-technical coworkers. How did you go about doing it?

My Streamlit server runs on localhost of my VM (guest) on port 50000 (or whatever port you want), my VM is Linux so my localhost address is 10.0.2.15. In my Virtualbox settings I have port forwarding enabled from guest to host. In my host (Windows) I had to enable firewall exceptions for port 50000, if I want other people to see my server, I simply share the network address of my host (for example 192.168.0.x) and the port (50000) and as long as the VM is running, people can see my streamlit app via their browser at the address: http://192.168.0.x:50000

Not sure if creating a batch file (as a one-click button) to open the environment
can do what you want…

(04:14 from the following video)

I Discovered a Way By which you can make an executable file without Pyinstaller
Just Click This Link And Read It I’ve Listed the Steps

1 Like

Dear all,

thanks a lot for meaningful discussion.
I was also faced with the same problem.

Now, I found a solution without 404 not found error.

Environment

  • python = 3.7.9
  • streamlit = 0.71.0
  • pyinstaller = 4.1

After that, suppose we want to make an executable file from the following main.py:

[main.py]

import streamlit as st

if __name__ == '__main__':
    st.header("Hello world")

Method

  1. Wrap the main script.

    • Make a wrapper script run_main.py:
    • Add the following lines to cli.py contained in the streamlit distribution, e.g. ${YOUR_CONDA_ENV}/lib/site-packages/streamlit/cli.py:

[run_main.py]

import streamlit.cli

if __name__ == '__main__':
    streamlit.cli._main_run_clExplicit('main.py', 'streamlit run')

[cli.py]

def _main_run_clExplicit(file, command_line, args=[ ]):
    streamlit._is_running_with_streamlit = True
    bootstrap.run(file, command_line, args)
  1. Create ./hooks/hook-streamlit.py:

[hook-streamlit.py]

from PyInstaller.utils.hooks import copy_metadata
datas = copy_metadata('streamlit')
  1. Create ./.streamlit/config.toml:

[config.toml]

[global]
developmentMode = false

[server]
port = 8501
  1. (NEW) Edit run_main.spec which is created after pyinstaller --onefile --additional-hooks-dir=./hooks run_main.py --clean:

[run_main.spec]

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(['run_main.py'],
             pathex=['.'],
             binaries=[],
             datas=[
                 (
                     "{$YOURPYTHONENV}/Lib/site-packages/altair/vegalite/v4/schema/vega-lite-schema.json",
                     "./altair/vegalite/v4/schema/"
                 ),
                 (
                     "${YOURPYTHONENV}/Lib/site-packages/streamlit/static",
                     "./streamlit/static"
                 )
            ],
            ...,
            noarchive=False)
pyz = PYZ(...)
exe = EXE(...)
  1. Finally, execute pyinstaller --onefile --additional-hooks-dir=./hooks run_main.spec --clean.

Directory

WORKINGDIR/
    - .streamlit/
        - config.toml
    - hooks/
        - hook-streamlit.py
    - main.py
    - run_main.py
    - run_main.spec
    - build/
        - run_main/
            - many .toc and .pyz
    - dist/
        - run_main.exe

NOTE

The executable file created above does not work alone.
You should copy .streamlit and main.py into dist direcoty.

Thank you :grinning:

5 Likes

I can confirm the method from @hmasdev.
This is awesome, thanks a lot!
It is even possible to edit the streamlit script (main.py in this case) during runtime as usual.

Nicely done!

Random (delusional?) thoughts…

Instead of making a streamlit binary for one app:

  • we could maybe create a generic streamlit runner that can execute any script passed in argument (or with a drag & drop onto the .exe)
  • and maybe associate an extension like myapp.st / myapp.stpy so that it runs with that streamlit runner :smiley:
  • and maybe we could modify that specific version of streamlit to run in an electron app (cf. this issue)

There’s still the case of particular imports in your app, but we could imagine creating different streamlit runners which bundle commonly used libraries :smiley:

4 Likes

Don’t know why.
I copied your code and it worked at the beginning.
But when I imported some packages (like matplotlib) into main.py, the executable failed to be created. :frowning:

What kind of error message do you get? In my case it helped to modify recursion limit, see below. You could also try to define matplotlib as hidden import. This was necessary for some packages that weren´t imported correctly by PyInstaller.

[run_main.spec]

>  #-*- mode: python ; coding: utf-8 -*-
> import sys
> sys.setrecursionlimit(sys.getrecursionlimit() * 5)
>
> block_cipher = None
> 
> a = Analysis(['run_main.py'],
>              pathex=['.'],
>              binaries=[],
>              datas=[
>                  (
>                      "{$YOURPYTHONENV}/Lib/site-packages/altair/vegalite/v4/schema/vega-lite-schema.json",
>                      "./altair/vegalite/v4/schema/"
>                  ),
>                  (
>                      "${YOURPYTHONENV}/Lib/site-packages/streamlit/static",
>                      "./streamlit/static"
>                  )
>             ],
>             hiddenimports=['matplotlib'],
>             ...,
>             noarchive=False)
> pyz = PYZ(...)
> exe = EXE(...)
1 Like

I took a different approach to this. I used pyinstaller to create the single file exe, but the resulting Python used was actually a separate install, not the one made by pyinstaller. That’s so that my users can create new streamlit apps and use new sections of the dependencies that I wasn’t using. (Given pyinstaller would have stripped those out).

To see a demo of the exe see:

When first clicking it, it will unpack the distro. Subsequent clicks should be much quicker to boot. I need to make the terminal give the user some details and provide functionality for updates etc. But in its current form it is usable.

The script used to make that exe can be found over at:

And here is the pyinstaller magic. The following script is what pyinstaller is pointed to. It sees if the full streamlit python distro has been extracted, if it has it boots up streamlit in there, if it hasn’t it extracts it before doing so:

This whole approach should be easier now that I suspect the following PR has just released within 0.72.0:

All that will be required now is changing the following line:

To calling “python.exe -m streamlit run path/to/your-app.py”

Very interesting, thank you for the step by step guide, I’ve managed to get it working!

However, can I check how long does your .exe take to load? Mine seems to take in excess of 1 minute, which seems pretty long. I’m already using a freshly created virtual environment, with only streamlit/pyinstaller installed.