DEV Community

Ngonidzashe Nzenze
Ngonidzashe Nzenze

Posted on

Using pyinstaller to package kivy and kivyMD desktop apps

Packaging kivy applications can be quite daunting. The documentation can be pretty confusing at times. This guide will walk you through how to package a kivyMD application for windows using pyinstaller through the use of an simple example.

Let's get right into it!πŸ™‚

I assume that you already have kivy and kivyMD installed in a virtual environment.

The first step is to install pyinstaller with pip. Open up the command prompt terminal, activate your virtual environment and type in the following:

pip install pyinstaller

Create two files:

  • main.py - which is going to contain the main code.
  • main.kv - which is going to contain the code written in kivy lang

Note: These files should be in the same directory.

Inside main.py, enter the following code:

import os, sys
from kivy.resources import resource_add_path, resource_find
from kivymd.app import MDApp

class MainApp(MDApp):
    def build(self):
        self.theme_cls.primary_palette = "DeepPurple"

if __name__ == '__main__':
    try:
        if hasattr(sys, '_MEIPASS'):
            resource_add_path(os.path.join(sys._MEIPASS))
        app = MainApp()
        app.run()
    except Exception as e:
        print(e)
        input("Press enter.")
Enter fullscreen mode Exit fullscreen mode

The above code is very crucial when it comes to packaging our application. The try-catch statement helps us identify errors in our program and the input("Press enter.") stops the console from closing before we want it to.

Edit main.kv:

MDRectangleFlatButton:
    text: "Hello World"
    pos_hint: {'center_x': .5, 'center_y': .5}
Enter fullscreen mode Exit fullscreen mode

We are just going to display a simple button.

Save the files then open the command prompt terminal, activate your environment and navigate to the folder containing the two files.

Now, before you even start the packaging process, make sure when you run your application you are not getting any console errors.

Type pyinstaller --onefile main.py in command prompt and press enter.

When execution is complete, two folders should have been created in the directory containing main.py; dist and build and a file named main.spec.

Navigate into the dist folder. Inside it, there is an executable file, main.exe. If you double click it, a terminal will open and close very quickly.

Window quickly opens and closes

To solve this issue, add the following lines to the top of main.spec:

from kivy_deps import sdl2, glew
from kivymd import hooks_path as kivymd_hooks_path
Enter fullscreen mode Exit fullscreen mode

Note: If you are not packaging a kivyMD application, there is no need for the second import or modifying hookspath as shown shortly below.

Add in Analysis the lines:

a = Analysis(
    #...
    datas=[('main.kv', '.')],
    hookspath=[kivymd_hooks_path],
   #...
)
Enter fullscreen mode Exit fullscreen mode

datas is made up of a list of tuples where the first item in the tuple is the file name and the second item is the directory that is going to be used to store the file. If for example, you had a folder named images in the same directory as main.py, you would add them as follows:

datas=[('main.kv', '.'), ('./images/*.png', 'images')],

The above line is basically saying, 'Get all the png images in the images folder and save them to a folder named images in the final application.'

Next, edit EXE:

exe = EXE(
    #...
    *[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
    #....
)
Enter fullscreen mode Exit fullscreen mode

The final main.spec file looks like this:

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

block_cipher = None
from kivy_deps import sdl2, glew
from kivymd import hooks_path as kivymd_hooks_path

a = Analysis(['main.py'],
             pathex=['C:\\Users\\path\\to\\file'],
             binaries=[],
             datas=[('main.kv', '.')],
             hiddenimports=[],
             hookspath=[kivymd_hooks_path],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          *[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
          name='main',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=True )
Enter fullscreen mode Exit fullscreen mode

Now save and run: pyinstaller main.spec in the terminal.

After it has finished executing, try executing main.exe again.

It works

And that's it! You have successfully packaged your application.

Errors you may encounter

You may encounter import errors when running pyinstaller. I encountered errors importing pyenchant and cv2. These can easily be fixed by running the following commands in your terminal:

pip install pyenchant
pip install opencv-python

After the installation is complete, run pyinstaller main.spec in your terminal and try executing the main.exe again.

Final thoughts

I hope this guide can prove useful to you. It is by no means a complete guide covering all the aspects of packaging kivy application for windows but I hope you can learn something from it.

For more information, check out:

Top comments (20)

Collapse
 
nikthegiant profile image
Nikthegiant

i get an error if i do console= false

Error:

Traceback (most recent call last):
File "logging_init_.py", line 1103, in emit
AttributeError: 'NoneType' object has no attribute 'write

Collapse
 
ngonidzashe profile image
Ngonidzashe Nzenze

Hi,

If you are using pyinstaller 5.7.0, try building your app with version 5.6.2

Collapse
 
nikthegiant profile image
Nikthegiant

Doesn't work... wich Versions should i Take from kivymd or kivy?

Thread Thread
 
ngonidzashe profile image
Ngonidzashe Nzenze

I'm using kivy==2.1.0 and kivymd==1.1.1.

Did you try running it with --noconsole?
That is, pyinstaller --onefile --noconsole main.py

Thread Thread
 
nikthegiant profile image
Nikthegiant

Yes, that is the problem. With console it worked, but without console It doesn't.

Thread Thread
 
ngonidzashe profile image
Ngonidzashe Nzenze

Please share your spec file

Thread Thread
 
nikthegiant profile image
Nikthegiant

The Standard Hook from the KIVYMD webside,
i think i must Import hidden modules, but i don't know how i do it correctly...

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

import sys
import os

from kivy_deps import sdl2, glew

from kivymd import hooks_path as kivymd_hooks_path

path = os.path.abspath("E:\Programmieren\Python\KivyMD\CreateApp")

a = Analysis(
["main.py"],
pathex=[path],
hookspath=[kivymd_hooks_path],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=None)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
*[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
debug=False,
strip=False,
upx=True,
name="Main",
console=False,
)`

Thread Thread
 
ngonidzashe profile image
Ngonidzashe Nzenze

I used your spec file and it built the app successfully. I faced the same issue before and solved it by simply changing the version of pyinstaller I was using to version 5.6.2.

Are getting that error from building the app in this tutorial or your own application?

Thread Thread
 
nikthegiant profile image
Nikthegiant

i did it with pyinstaller==5.6.2 and it worked!! thank you so much!!!!

Thread Thread
 
ngonidzashe profile image
Ngonidzashe Nzenze

You're welcome. Glad it worked

Collapse
 
islamimtiaz profile image
IslamImtiaz • Edited

Greetings, I am currently attempting to transform my python code into an APK, however, after converting it and installing the file, it unfortunately crashes right after the kivy logo appears. I am using google collab to convert my code into APK.
Spec file
Image description

main.py

from kivymd.uix.screen import MDScreen
from kivymd.app import MDApp
from kivy.uix.image import Image
from kivymd.uix.button import MDFillRoundFlatButton
from kivymd.uix.textfield import MDTextField
from kivymd.uix.label import MDLabel
from kivymd.uix.toolbar import MDTopAppBar
from kivymd.uix.dialog import MDDialog

class ConverterApp(MDApp):

    def flip(self):
        if self.state == 0:
            self.state = 1
            self.toolbar.title = 'CGPA calculator'
            self.GP.hint_text = 'Enter your current GPA'
            self.CH.hint_text = 'Enter your previous GPA'
        else:
            self.state = 0
            self.toolbar.title = 'GPA calculator'
            self.GP.hint_text = 'Enter your GP'
            self.CH.hint_text = 'Enter subject CH'

    def gpa(self, obj):
        gp_text = self.GP.text.strip()
        ch_text = self.CH.text.strip()
        # Check if GP and CH fields are not empty
        if not gp_text or not ch_text:
            # Show an error when GP and CH fields are empty
            dialog = MDDialog(title='Error',text='Both GP and CH fields are required',size_hint=(0.7, 1))
            dialog.open()
            return
        # Check if entered data is valid
        try:
            gp_values = [float(gp) for gp in gp_text.split(',')]
            ch_values = [float(ch) for ch in ch_text.split(',')]
        except ValueError:
            # Show an error when non-numeric value is entered
            dialog = MDDialog(title='Error',text='Invalid input! Please enter comma-separated numbers only',size_hint=(0.7, 1))
            dialog.open()
            return
        # Calculate GPA or CGPA
        if self.state == 0:
            x = sum(gp_values)
            y = sum(ch_values)
            if y == 0:
                # Show an error
                dialog = MDDialog(title='Error',text='Zero division error',size_hint=(0.7, 1))
                dialog.open()
            else:    
                c = x / y
                self.label1.text = str(c)
                self.label.text = "Your GPA is: "
        else:
            x = sum(gp_values)
            len1 = len(gp_values)
            y = sum(ch_values)
            len2 = len(ch_values)
            b = len1 + len2
            z = (x + y) / b
            self.label1.text = str(z)
            self.label.text = 'Your CGPA is: '

    def build(self):
        self.state = 0
        screen = MDScreen()
        # Top toolbar
        self.toolbar = MDTopAppBar(title="GPA Calculator")
        self.toolbar.pos_hint = {'top':1}
        self.toolbar.right_action_items = [['rotate-3d-variant', lambda x: self.flip()]]
        screen.add_widget(self.toolbar)
        # Logo
        screen.add_widget(Image(source ='gpr.png',size_hint=(0.8,1),pos_hint ={'center_x':0.5,'center_y':0.7}))
        # Collect user input
        self.GP = MDTextField(hint_text='Enter your GP',halign='center',size_hint=(0.8,1),pos_hint ={'center_x':0.5,'center_y':0.48},font_size=22)
        screen.add_widget(self.GP)
        self.CH = MDTextField(hint_text='Enter your CH',halign='center',size_hint=(0.8,1),pos_hint ={'center_x':0.5,'center_y':0.4},font_size=22)
        screen.add_widget(self.CH)
        # Secondary + Primary Label
        self.label = MDLabel(halign='center',pos_hint ={'center_x':0.5,'center_y':0.32},theme_text_color='Secondary')
        self.label1 = MDLabel(halign='center',pos_hint ={'center_x':0.5,'center_y':0.28},theme_text_color='Primary',font_style='H5')
        screen.add_widget(self.label)
        screen.add_widget(self.label1)
        # Convert Button
        self.button= MDFillRoundFlatButton(text='Result',font_size='17',pos_hint ={'center_x':0.5,'center_y':0.15})
        self.button.bind(on_press = self.gpa)
        screen.add_widget(self.button)

        return screen

ConverterApp().run()
Enter fullscreen mode Exit fullscreen mode
Collapse
 
panxproject profile image
Waleed Sadek • Edited

The problem you're experiencing might be due to a number of reasons, including missing dependencies, issues with your buildozer.spec file, or problems with the build process itself.

Here are a few steps you can follow to help debug and solve the problem:

  1. Check the Logs: After running the application and it crashes, check the logs for any error messages. On an Android device, you can view the logs by connecting the device to your computer and running adb logcat in a terminal. This should provide some information about why the application is crashing.

  2. Check the Buildozer.spec File: Ensure that all necessary requirements are mentioned in the buildozer.spec file. You can use the requirements field in the spec file to specify all Python modules that your app depends on.

  3. Permissions: Some functionalities might require certain permissions to be set in the buildozer.spec file. If your app is trying to access a feature it does not have permission for, it could cause the app to crash.

  4. Resource Files: If your application uses any resources such as images, audio files, etc., make sure that these files are being included in the APK. In your buildozer.spec file, the source.include_exts option can be used to specify the file types to include in the package.

  5. Python Version: Make sure you're using a Python version that's compatible with KivyMD.

Share the log with the errors showing, it will be helpful to debug!

Collapse
 
mohsenmrds profile image
mohsenmrds

That's completely nice and worked for me. thank you very much.
The question is why the above solution is not in pyinstaller by default?

Collapse
 
ngonidzashe profile image
Ngonidzashe Nzenze

You're welcome. Glad it worked for you!

Collapse
 
drizzyovo profile image
Gaurav Malpedi

Hi, your code did help me. Thank you. But can you please show me how to include a text file or a JSON file as a database. I wanna make a kivy desktop application in onefile. It's giving me error. Please help me out.

Collapse
 
ngonidzashe profile image
Ngonidzashe Nzenze

Hi,

So the best I can do is give you an example using json. You'll need two file, main.py and main.kv. I'm going to be using code from this article to demonstrate.

main.kv remains the same as in the article but in main.py I am going to add code to save and extract data from a json file so in the end it will look like this:

import os, sys, json
from kivy.resources import resource_add_path, resource_find
from kivymd.app import MDApp
from kivymd.uix.list import OneLineIconListItem, IconLeftWidget

def save_data_to_json(filename, data):
    with open(filename, "w") as file:
        json.dump(data, file)


# Extract data from a JSON file
def extract_data_from_json(filename):
    with open(filename, "r") as file:
        return json.load(file)

class MainApp(MDApp):
    def build(self):
        self.theme_cls.primary_palette = "Purple"

    def add_item(self, text):
        new_list_item = OneLineIconListItem(text=text)
        new_list_item.add_widget(IconLeftWidget(icon="language-python"))
        self.root.ids.listcontainer.add_widget(new_list_item)
        self.root.ids.listinput.text = ""

        # Check if the JSON file exists
        if os.path.exists("data.json"):
            # Extract existing data from the JSON file
            data = extract_data_from_json("data.json")

        else:
            data = []

        # Add the new item to the data
        data.append(text)

        # Save the updated data to the JSON file
        save_data_to_json("data.json", data)

    def on_start(self):
        """Load items from the JSON file on app start"""

        # Check if the JSON file exists
        if os.path.exists("data.json"):
            # Extract data from the JSON file
            data = extract_data_from_json("data.json")

            # Iterate over the data and add items to the list container
            for item in data:
                self.add_item(item)


if __name__ == "__main__":
    try:
        if hasattr(sys, "_MEIPASS"):
            resource_add_path(os.path.join(sys._MEIPASS))
        app = MainApp()
        app.run()
    except Exception as e:
        print(e)
        input("Press enter.")

Enter fullscreen mode Exit fullscreen mode

When you add an item, it will be added to the listbox and saved to the json file as well. When you start the app, data is loaded from the json file and added to the listbox.

Hope that helps.

Collapse
 
james_hanenian_3a5fa77ddb profile image
James Hanenian

Hi, when I run pyinstaller --onefile main.py with your example, I get an empty dist folder with no executable file in it. I have python==3.10.8, kivy==2.1.0, kivymd==1.1.1.
I even tried changing pyinstaller version to 5.6.2 and the hello world program runs normally.
I am not sure how to proceed from here.

Collapse
 
spirich profile image
413K54HDR

Thanks, it worked!

Collapse
 
samueljevix profile image
samueljevix

thanks bro it worked for me

Collapse
 
murphenheim profile image
samad

for linux:
just skip the kivy_deps import .. and *[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)], steps