Search code examples
pythonsetuptoolsdistutils

Create launchable GUI script from Python setuptools (without console window!)


The way I currently add an executable for my Python-based GUI is this:

setup(
      # ...
      entry_points = {"gui_scripts" : ['frontend = myfrontendmodule.launcher:main']},
      # ...
      )

On Windows, this will create "frontend.exe" and "frontend-script.pyw" in Python's scripts folder (using Python 2.6). When I execute the EXE file, a console window is shown but the PYW file works correctly without showing one.

So my question is: How can I make the EXE file execute the program without the console window? The solution should work on Linux, too (don't suggest py2exe ;).


Solution

  • Alright, I investigated a bit in the setuptools source code and it all boils down to a bug in setuptools (easy_install.py):

    # On Windows/wininst, add a .py extension and an .exe launcher
    if group=='gui_scripts':
        ext, launcher = '-script.pyw', 'gui.exe'
        old = ['.pyw']
        new_header = re.sub('(?i)python.exe','pythonw.exe',header)
    else:
        ext, launcher = '-script.py', 'cli.exe'
        old = ['.py','.pyc','.pyo']
        new_header = re.sub('(?i)pythonw.exe','python.exe',header)
    
    if os.path.exists(new_header[2:-1]) or sys.platform!='win32':
        hdr = new_header
    else:
        hdr = header
    

    The last if statement decides whether pythonw.exe's or python.exe's path is written into the shebang of "frontend-script.pyw". As this shebang is evaluated by the created EXE file, it is necessary that the else statement is not executed. The problem is that new_header[2:-1] in my case was "C:\Program Files (x86)\Python26\pythonw.exe" (with the quotes!), so os.path.exists said it does not exist because of the quotes.

    I will try to get this corrected by the setuptools developers. The remaining problem will be the absolute pythonw.exe path. If I create a Windows setup/MSI installer, the shebang will contain my pythonw.exe path ("C:\Program Files (x86)\Python26\pythonw.exe") but the user might have installed Python in "C:\Python26". I'll report the final solution after I've reported this problem.


    I posted this over two years back, sorry that I didn't yet offer my solution. Not sure if there is any more modern solution (probably distribute offers something), but here's what I used back then (copy-pasted):

    File dogsync-frontend-script.pyw

    #!pythonw.exe
    
    # This script will be executed by the primary Python version that is installed, which might as well be Python 3. But
    # we want to execute it with the Python version that belongs to this script's path. So let's do a major hack:
    
    import os
    import sys
    import subprocess
    
    if sys.argv[-1] == "magic":
        from dogsync_frontend.launcher import main
        main()
    else:
        # The CPython folder hierarchy is assumed here (<installation>\pythonw.exe, <installation>\Scripts\<thisscript>)
        subprocess.Popen([os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "pythonw.exe")),
                          __file__,
                          "magic"])
    

    File dogsync-frontend.exe

    Automatically copied from <python installation>\lib\site-packages\setuptools\gui.exe (see below). This file will automatically execute the script <name of EXE>-script.py[w] if I remember correctly.

    File setup.py

    from setuptools import __file__ as setupToolsFilename
    
    if os.name == "nt":
        # Use a customized (major hack) start script instead of the one that gets automatically created by setuptools
        # when the "gui_scripts" parameter is used. This way, we don't need setuptools installed in order to run DogSync.
        shutil.copy2(os.path.join(os.path.dirname(setupToolsFilename), "gui.exe"),
                     "build-environment/windows-scripts/dogsync-frontend.exe")
        startScripts = dict(scripts = ["build-environment/windows-scripts/dogsync-frontend-script.pyw",
                                       "build-environment/windows-scripts/dogsync-frontend.exe"])
    else:
        # For Linux, I don't have a solution to remove the runtime dependency on setuptools (yet)
        startScripts = dict(entry_points = {"gui_scripts" : ['dogsync-frontend = dogsync_frontend.launcher:main']})
    
    setup(<other options>,
          **startScripts)
    

    With this setup, the exe/pyw files are copied to <python installation>\Scripts (on Windows) and starting dogsync-frontend.exe will run the pyw script without a console. Since setuptools did not get any updates for years, this solution is still working.