Skip to content

Chapter 08. Building the tool

We come to our last step. We already cover the different types of audiences we may have. But let's revise it from Chapter 06:

Usage / Requirements Endpoints Built
Sandbox - -
Library - -
CLI x (Not necessarily)
API x (Running as a service)
GUI x x

We only require to really build an exe when having a stand alone GUI. However we will see two ways of delivering our tool here: 1. As a built package. 2. As an exe.

Building and publishing our package.

Throughout the entire Dikes For Dummies workshop we have been using Poetry. If your dependencies are still holding up and your project is well structure you should not have too much troubles building and publishing it. Let's check it.

poetry check

All set!

poetry build & poetry publish

You will be required to authenticate yourself in pypi

Notice that most likely a dist directory has been created in your root directory with the wheels to be published. After publishing our package we should be able to add it as a dependency on other projects!

poetry add dikes-for-dummies

As an exe

This step will require (a bit) more of work. First we require the pyinstaller package (poetry add pyinstaller --group dev).

An ideal world

In theory, the following should be possible:

  • Building only the CLI: poetry run pyinstaller dikesfordummies\main.py
  • Building with GUI: poetry run pyinstaller dikesfordummies\gui\main.py

However, it is entirely possible that as more complex your repository starts to be, the more dependencies you need to specify by yourself. This might result on you having to create your custom main.specfile and your own compilation script for pyinstaller. We will describe these steps in the next sections. For that, lets create both files in a \makefile dir in our root.

init.py

Because we want our scripts to be findable and executable, we can create an __init__.py file that will also provide us some help:

from pathlib import Path

import dikesfordummies

_makedir = Path(__file__).parent
_dfd_version = dikesfordummies.__version__

main.spec

# -*- mode: python -*-
from PyInstaller.utils.hooks import collect_data_files, collect_dynamic_libs
import glob, os
from pathlib import Path
from makefile import _makedir

_conda_env = os.environ['CONDA_PREFIX']

_root_dir = _makedir.parent
_dfd_src = _root_dir / "dikesfordummies"

a = Analysis([r"..\\dikesfordummies\\gui\\main.py"],
             pathex=['.', str(_dfd_src), _conda_env],
             hiddenimports=[],
             hookspath=None,
             runtime_hooks=None,
             datas=[],
             binaries= collect_dynamic_libs("rtree"),)

for d in a.datas:
    if 'pyconfig' in d[0]: 
        a.datas.remove(d)
        break

print("Generate pyz and exe")
pyz = PYZ(a.pure)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name='DikesForDummies.exe',
          debug=False,
          strip=False,
          upx=True,
          console=False,
        )

Defining our custom compiler

import datetime
import os
import shutil
import sys
from pathlib import Path

from makefile import _dfd_version, _makedir

# __version__ is automatically updated by commitizen, do not change manually.
# Run insted 'cz bump --changelog' and then 'git push --tags' and 'git push'.
_version_file = _makedir / "version.txt"
_main_spec = _makedir / "main.spec"

def read_revision():
    # date
    now = datetime.datetime.now()
    if _version_file.is_file():
        _version_file.unlink()

    _version_as_string = _dfd_version.replace(".", ",")
    _vs_version_info = f"{_version_as_string}, 0"
    with _version_file.open("w") as f:
        f.write(
            "VSVersionInfo(\n"
            + "ffi=FixedFileInfo(\n"
            + f"filevers=({_vs_version_info}),\n"
            + f"prodvers=({_vs_version_info}),\n"
            + "mask=0x3f,\n"
            + "flags=0x0,\n"
            + "OS=0x40004,\n"
            + "fileType=0x1,\n"
            + "subtype=0x0,\n"
            + "date=(0, 0)\n"
            + "),\n"
            + "kids=[\n"
            + "StringFileInfo(\n"
            + "[\n"
            + "StringTable(\n"
            + "u'040904B0',\n"
            + "[StringStruct(u'CompanyName', u'Dummies'),\n"
            + "StringStruct(u'FileDescription', u'DikesForDummies'),\n"
            + "StringStruct(u'InternalName', u'DikesForDummies'),\n"
            + "StringStruct(u'LegalCopyright', u'Dummies"
            + r" \xae "
            + str(now.year)
            + "'),\n"
            + "StringStruct(u'OriginalFilename', u'DikesForDummies.exe'),\n"
            + "StringStruct(u'ProductName', u'DikesForDummies"
            + r" \xae "
            + "Dummies'),\n"
            + f"StringStruct(u'ProductVersion', u'{_dfd_version}')])\n"
            + "]), \n"
            + "VarFileInfo([VarStruct(u'Translation', [1033, 1200])])\n"
            + "]\n"
            + ")"
        )

def compile_code():
    def _remove_if_exists(dir_name: str):
        _dir_to_remove = _makedir.parent / dir_name
        if (_dir_to_remove).is_dir():
            shutil.rmtree(_dir_to_remove)

    _remove_if_exists("dist")
    _remove_if_exists("build")
    _py_installer_exe = Path(sys.exec_prefix) / "Scripts" / "pyinstaller.exe"
    assert _py_installer_exe.is_file()
    os.system(f"{_py_installer_exe} --clean {_main_spec}")
    _version_file.unlink()

def run_compilation():
    read_revision()
    compile_code()

if __name__ == "__main__":
    run_compilation()

Let's run it!

poetry run python makefile\version_compile.py

After some time you should find in /dist your DikesForDummies.exe

Extending our poetry config

We can create a 'shortcut' to our compilation script so that with a single poetry run build-exe call everthing is executed within our safe virtual environment.

[tool.poetry.scripts]
build-exe = "makefile.version_compile:run_compilation"

Summary

We have finaly covered all steps required to build an MVP. There are many different ways to come to this final step, however, that is the nice challenge about python. Explore all its possibilities and don't be shy about asking or sharing your progress. Happy coding!