Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nuitka stand alone executables #296

Open
wants to merge 5 commits into
base: pyside6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions build_tools/build_script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Build executable with Nuitka."""
from pathlib import Path
from platform import system
from shutil import copytree, rmtree

import ui_to_py as uipy

# Build config and file paths
code_dir = Path("ibridgesgui")
icons_folder = Path.cwd().joinpath(code_dir, 'icons')
ui_folder = Path.cwd().joinpath(code_dir, 'ui_files')
venv = Path.cwd().joinpath('venv')
# Debug mode shows the console (command prompt with the logging)
debug_mode = True
# ---------------------------------------------------------#


def replace_folder(source: Path | str, destination: Path | str):
"""Replace the destination folder with the source folder."""
copytree(source, destination, dirs_exist_ok=True)


def create_exe():
"""Create the exe file for the application."""
(python, cmd_sep) = uipy.os_specific_settings()

# Step 1: Setup
# Step 1a, Ensure the folder for the venv exists
if (not venv.exists()) or (not venv.is_dir()):
venv.mkdir()

# Step 1b, Create the venv if needed
# windows
if system()[0].upper() == "W":
venv_activate = venv.joinpath('Scripts', 'activate.bat')
else: # Ubuntu/IOS
venv_activate = venv.joinpath('bin', 'activate')
if (not venv_activate.exists()) or (not venv_activate.is_file()):
venv_activate = f"\"{str(venv_activate)}\""
if system()[0].upper() != "W":
venv_activate = f"source {venv_activate}"
uipy.run_cmd(f"{python} -m venv {venv}")
uipy.run_cmd(f"{venv_activate} {cmd_sep} python -m pip install --upgrade pip")
uipy.run_cmd(f"{venv_activate} {cmd_sep} pip install -r requirements.txt")
else:
venv_activate = f"\"{str(venv_activate)}\""

# Step 2 Convert .ui files to .py files
# Recompiling is the best way to ensure they are up to date
uipy.remove_pyui_files(ui_folder)
uipy.ui_to_py(ui_folder, venv_activate, cmd_sep)

# Step 3, Activate venv and run nuitka
cmd = f"{venv_activate} {cmd_sep} python -m nuitka "
if not debug_mode:
cmd += "--disable-console "
cmd += f"--standalone --include-package=irods --nofollow-import-to=irods.test\
--remove-output --enable-plugin=pyside6 --include-qt-plugins=sensible,styles \
--assume-yes-for-downloads --show-progress \
--windows-icon-from-ico=\"{icons_folder.joinpath('iBridges.ico')}\" \
{code_dir.joinpath('__main__.py')} --quiet"
uipy.run_cmd(cmd)

# Step 4, move the icons folder to the distribution folder
replace_folder(icons_folder,
icons_folder.parent.parent.joinpath('__main__.dist',
icons_folder.name))

# Step 5, rename the distribution folder and file
shipping_folder = Path('output/ibridgesgui')
if Path(shipping_folder).exists():
rmtree(shipping_folder, ignore_errors=True)
shipping_folder.parent.mkdir(parents=True, exist_ok=True)
Path('__main__.dist').rename(shipping_folder)
Path(f'{shipping_folder}/__main__.exe').rename(f'{shipping_folder}/ibridges.exe')


if __name__ == "__main__":
create_exe()
110 changes: 110 additions & 0 deletions build_tools/ui_to_py.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Convert Ui file to PY file."""
from pathlib import Path
from platform import system
from subprocess import STDOUT, run


def run_cmd(cmd: str):
"""Run a commandline command and check if it was successful.

Args:
cmd : str
Command to run.

"""
if system()[0].upper() == "W": # Windows
ps = run(cmd, stderr=STDOUT, shell=True, universal_newlines=True)
else:
ps = run(cmd, stderr=STDOUT, shell=True, universal_newlines=True, executable="/bin/bash")
# Print all errors
if ps.stderr is not None or ps.returncode != 0:
print(f"commandline error: {ps.stderr}")
raise Exception("shell run error")


def os_specific_settings():
"""Get the settings for the operating system.

Returns:
python : str
python version
cmd_sep : str
command separator

"""
if system()[0].upper() == "W": # windows
cmd_sep = "&&"
python = "python" # python version
else: # Linux, ensure it uses python 3
cmd_sep = ";"
python = "python3"
return (python, cmd_sep)


def ui_to_py(ui_folder: Path, venv_activate: str, cmd_sep: str):
"""Convert the .ui files to .py files.

pyside6-uic gui/MainWindow.ui -o gui/MainWindow.py
Args:
ui_folder : Path
folder containing the .ui files
venv_activate : str
command to activate the virtual environment
cmd_sep : str
command separator

"""
for ui_file in ui_folder.glob('*.ui'):
py_file = ui_file.with_suffix('.py')
print(f"Converting {py_file.name} to .py")
run_cmd(f"""{venv_activate} {cmd_sep} pyside6-uic "{ui_file}" -o "{py_file}" """)
replace_icon_paths(py_file)


def replace_icon_paths(py_file: Path):
"""Replace the icon paths in the .py file.

Args:
py_file : Path
.py file to update

"""
# Read the content of the file
with py_file.open('r', encoding='utf-8') as file:
content = file.read()

# Replace the icon paths
content = content.replace('addFile(u"../icons/', 'addFile(u"icons/')
# Write the modified content back to the file
with py_file.open('w', encoding='utf-8') as file:
file.write(content)


def remove_pyui_files(ui_folder: Path):
"""Remove the locally stored .py versions of the files.

Args:
ui_folder : Path
folder containing the .ui files

"""
pyuifiles = ui_folder.glob('*.py')
for file in pyuifiles:
# Skip __init__.py files
if "__init__" in file.name:
continue
print(f"Removing {file}")
file.unlink()


if __name__ == "__main__":
code_dir = Path("ibridgesgui")
icons_folder = Path.cwd().joinpath(code_dir, 'icons')
ui_folder = Path.cwd().joinpath(code_dir, 'ui_files')

# Step 1 Convert .ui files to .py files
# Recompiling is the best way to ensure they are up to date
remove_pyui_files(ui_folder)
(python, cmd_sep) = os_specific_settings()
venv_activate = Path.cwd().joinpath('venv').joinpath('Scripts', 'activate.bat')
ui_to_py(ui_folder, venv_activate, cmd_sep)
10 changes: 7 additions & 3 deletions ibridgesgui/gui_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import os
import pathlib
import sys
from typing import Union

import irods
Expand All @@ -20,9 +21,12 @@
except ImportError:
from importlib_resources import files


UI_FILE_DIR = files(__package__) / "ui_files"
LOGO_DIR = files(__package__) / "icons"
if getattr(sys, "frozen", False) or ("__compiled__" in globals()):
UI_FILE_DIR = pathlib.Path("ui_files")
LOGO_DIR = pathlib.Path("icons")
else:
UI_FILE_DIR = files(__package__) / "ui_files"
LOGO_DIR = files(__package__) / "icons"


class UiLoader(PySide6.QtUiTools.QUiLoader):
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PySide6>=6.8.1
ibridges
pyinstaller==6.11.1
setproctitle==1.3.4
Nuitka>=2.5.8
Loading