Making win installer

This part gives a detailed step by step guide to make the windows installer.

Environment setup

First of all, we need to setup our environment with the required packages and libraries. We recommend creating a clean environment to make sure that only the necessary libraries are packaged. Here, we will explain how to create a new clean environment from scratch using conda.

  1. Create a new virtual environment
conda create -n yourenvname python=x.x

where:

  • yourenvname: is the name of your environment (ex: py37_urbs_gui).
  • x.x: is the desired python version (currently we use 3.7)
  1. Activate your virtual environment
activate yourenvname

3. Now, the environment is created and we are ready to install the necessary packages. The following table list all the needed packages. Please go through the list in order and start installing the packages one by one.

Table: Required packages
# Package name Version How to install?
1 Python 3.7  
2 wxPython 4.0.6 pip install wxPython
3 numpy 1.16.4 Installed automatically with wxPython
4 PyPubSub 4.0.3 pip install PyPubSub
5 pyomo 5.6.4 pip install pyomo
6 pandas 0.24.2 pip install pandas
7 xlrd 1.2.0 pip install xlrd
8 matplotlib 3.1.0 pip install matplotlib
9 pywin32 224 pip install pywin32
10 tables 3.5.2 pip install tables
11 openpyxl 2.6.2 pip install openpyxl
12 PyInstaller 3.4 pip install PyInstaller

The mentioned versions in the table above are the used, tested and verified ones. You can always go for the latest versions but pay attention to the troubleshooting section.

Installer setup

As the environment is created, all you need now is to go to ‘Installer’ directory and run the ‘make_installer.bat’ file. Make sure you are running the batch file in the clean environment we created earlier (for example, the used environment here is named ‘py37’)

../_images/cmd.png

After the batch file is executed, an exe file will be created in the ‘Installer/Output’ directory. With the clean environment, the size of the created installer will be almost around 39MByte only.

Troubleshooting

This is important section and you will have to keep an eye on it especially if you upgraded or added new packages.

  • Now, you have the installer ready in the ‘Installer/Output’ directory. You run it and just go Next with the wizard.
../_images/wizard.png
  • After the installation, you open the program (from start menus or from shortcut on desktop)
  • You can get the following message as start.
../_images/err1.png
  • The message doesn’t give a proper details about the error. So we want to debug further.
  • Go to the ‘Installer’ directory in our git repo, locate the file ‘urbs_gui.spec’.
  • Open the file ‘urbs_gui.spec’ with any text editor.
  • Change the line ‘console=False’ to be ‘console=True’

pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=True,
          name='urbs_gui',
          debug=False,
          strip=False,
          upx=True,
          console=False )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=False,
               name='urbs_gui')
  • Save the file.
  • Now, we will rebuild the installer file so we can get more knowledge about the error.
  • Run the ‘make_installer.bat’ file again to build the installer with the console option enabled.
  • As before, a new installer is created in the folder ‘Installer/Output’.
  • Open the installer, go through the wizard and install the program.
  • Now, go to the installation directory that you specified in the wizard. The default is ‘C:\urbs_gui’.
  • Open a command prompt and execute the exe file from there as following:
C:\urbs_gui>urbs_gui.exe
  • In the console, you can see the error details now. For instance:
../_images/err2.png
  • In general, 99% of the installer problems are coming from `Pyomo`_. Why? PyInstaller is figuring out the required imports from the explicit import statements in the code, and it is able to recursively inspect the necessary modules. But ‘Pyomo’ is doing that differently. They build a list of the needed libs and loop on programmatically.
  • Go to our git repo, under the ‘urbs’ folder.
  • Open the file ‘runfunctions.py’
import os
import pyomo.environ
from pyomo.opt.base import SolverFactory
from datetime import datetime, date
from .model import create_model
from .report import *
from .plot import *
from .input import *
from .validation import *
from .saveload import *

  • As you can see in the highlighted line above, ‘pyomo.environ’ is imported.
  • In your IDE (may be Pycharm), ctrl and click on that line. This will open the ‘_init_.py’ file of the ‘environ’ module.
  • You will find the following code for importing the required packages!
_packages = [
    'pyomo.common',
    'pyomo.opt',
    'pyomo.core',
    'pyomo.dataportal',
    'pyomo.duality',
    'pyomo.checker',
    'pyomo.repn',
    'pyomo.pysp',
    'pyomo.neos',
    'pyomo.solvers',
    'pyomo.gdp',
    'pyomo.mpec',
    'pyomo.dae',
    'pyomo.bilevel',
    'pyomo.scripting',
    'pyomo.network',
]
def _import_packages():
    #
    # Import required packages
    #
    for name in _packages:
        pname = name+'.plugins'
        try:
            _do_import(pname)
        except ImportError:
            exctype, err, tb = _sys.exc_info()  # BUG?
            import traceback
            msg = "pyomo.environ failed to import %s:\nOriginal %s: %s\n"\
                  "Traceback:\n%s" \
                  % (pname, exctype.__name__, err,
                     ''.join(traceback.format_tb(tb)),)
            # clear local variables to remove circular references
            exctype = err = tb = None
            # TODO: Should this just log an error and re-raise the
            # original exception?
            raise ImportError(msg)

        pkg = _sys.modules[pname]
        pkg.load()
  • As you can see from the above section, it dynamically load the above list of packages. Note: the code append ‘.plugins’ to the name.
  • If you recall the error we get for our installer, it was:
ModuleNotFoundError: No module named 'pyomo.common.plugins'

When I upgraded the version of pyomo, the list is changed and ‘pyomo.common’ for instance was not part of the previous list!

  • We need to add this missing module (or the hidden import) to our build script. To do so, open the ‘urbs_gui.spec’ file and check the hiddenimports list.
# -*- mode: python -*-

block_cipher = None


a = Analysis(['..\\gui\\gui.py'],
             pathex=['..', '..\\gui'],
             binaries=[],
             datas=[],
             hiddenimports=['pyomo.common.plugins',
                            'pyomo.opt.plugins',
                            'pyomo.core.plugins',
                            'pyomo.dataportal.plugins',
                            'pyomo.duality.plugins',
                            'pyomo.checker.plugins',
                            'pyomo.repn.plugins',
                            'pyomo.pysp.plugins',
                            'pyomo.neos.plugins',
                            'pyomo.solvers.plugins',
                            'pyomo.gdp.plugins',
                            'pyomo.mpec.plugins',
                            'pyomo.dae.plugins',
                            'pyomo.bilevel.plugins',
                            'pyomo.scripting.plugins',
                            'pyomo.network.plugins',
                            'pandas._libs.skiplist'
             ],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)
  • Make sure that all libs defined in the _packages list used by `Pyomo`_ are included in the hiddenimports list.
  • Now, we can rebuild the installer and make sure it can start normally.
  • If everything is ok, then turn the console to be Flase again and rebuild the installer for distribution.