Using PyInstaller (or similar) to create an executable

Hi, I want to build a one-click executable out of my code that replicates the terminal command:

streamlit run mycode.py

Is there any way to achieve this with PyInstaller or similar? The main motivation is to share the functionality of my app with others who only use windows or who only use computers where python (along with the required imports) is not installed.

4 Likes

Hi @s_mc and welcome to the forum! :wave:

The best way to share would be to run streamlit in a server and share the app url. Another option would be to create a docker image with your code and share the image, but this docker option could be a little complex for individuals who are using your app.

Currently, we donā€™t have a way to internally deploy your app within Streamlit, but we are working on a ā€œFor Teamsā€ offering that will add this functionality. If youā€™re interested in hearing more, here is a link where you can sign-up to get updates as they become available.

In the meantime, here is a link to a post that goes over some community driven ways to host your app.

Hope this helps answer your question, and feel free to let us know if this isnā€™t the functionality youā€™re looking for! :smiley:

UPDATE Not yet solved, but almost, further help appreciated!!!

I created the following wrapper script to allow any script utilitzing streamlit to be run by calling
python streamlitWrapper.py.

import runpy
runpy.run_module('streamlit.cli')
import streamlit.cli
import click
click.pass_context
if __name__ == '__main__':
    streamlit.cli._main_run_clExplicit('pythonScript.py', 'streamlit run')

Save the 7 lines above into another script called streamlitWrapper.py (or any other name). You will also need to edit the file cli.py contained in the streamlit distribution to include the following def:

def _main_run_clExplicit(file, command_line, args=[]):
    streamlit._is_running_with_streamlit = True
    Credentials.get_current().check_activated(auto_resolve=True)
    if version.should_show_new_version_notice():
        click.echo(NEW_VERSION_TEXT)
        bootstrap.run(file, command_line, args)

After all that, you can run your script using the usual python call python streamlitWrapper.py.

The next step is to run:

pyinstaller streamlitWrapper.py

And with some massaging you should receive an executable.

The problem now is, the pyinstaller executable starts streamlit on port 3000 instead of port 8501 and my browser (for reasons I donā€™t understand) canā€™t run my script when using streamlitWrapper.exe (this is the default name of the executable pyinstaller will produce), running python streamlitWrapper.py works exactly the same as when calling streamlit run pythonScript.py, but for some reason the pyinstaller executable doesnā€™t exactly replicate the functionality you get when running python streamlitWrapper.py.

3 Likes

I have discovered the reason why when using the bundled executable my web browser doesnā€™t load properly, it is due to a check performed in streamlit to establish whether streamlit was properly installed. Basically everytime streamlit is run, at some time the following is executed in config.py:

@_create_option("global.developmentMode", visibility="hidden", type_=bool)
    def _global_development_mode():
        """Are we in development mode.

        This option defaults to True if and only if Streamlit wasn't installed
        normally.
        """
        return (
            not util.is_pex()
            and "site-packages" not in __file__
            and "dist-packages" not in __file__
        )

This is called from Report.py which has the effect of setting the port to 3000:

def _get_browser_address_bar_port():
    """Get the report URL that will be shown in the browser's address bar.

    That is, this is the port where static assets will be served from. In dev,
    this is different from the URL that will be used to connect to the
    server-browser websocket.

    """
    if config.get_option("global.developmentMode") and config.get_option(
        "global.useNode"
    ):
        return 3000
    return config.get_option("browser.serverPort")

This call works normally when using either streamlit run pythonScript.py or python streamlitWrapper.py but (for whatever reason), once pyinstaller bundles streamlit into the executable, the above check fails and sends streamlit into development mode, and things stop working as expected. This is obviously the wrong behaviour on streamlitā€™s part, pyinstaller has almost certainly bundled everything correctly but streamlit is looking in the wrong place to check if it has been installed correctly. What do I need to modify to ensure that streamlit is satisfied that it has been properly installed when running from an executable? I get the feeling that modifying def _global_development_mode(): to always return True is probably a dangerous idea.

5 Likes

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!

3 Likes

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.

3 Likes

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

2 Likes

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:

11 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:

6 Likes