diff options
| -rw-r--r-- | justfile | 3 | ||||
| -rw-r--r-- | tools/build_gui.py | 100 |
2 files changed, 101 insertions, 2 deletions
@@ -8,6 +8,5 @@ gui: # Build the picostream GUI executable build-gui: uv pip install pyinstaller pyinstaller-hooks-contrib - FFI_FLAG := `uv run python -c 'import sys, os, glob; ffi_dll_path = next(glob.iglob(os.path.join(os.path.dirname(sys.executable), "DLLs", "libffi-*.dll")), None) if sys.platform == "win32" else None; ffi_dll_path and print(f"--add-binary \"{ffi_dll_path}{os.pathsep}.\"")'` - uv run pyinstaller --onefile --windowed --name PicoStream {{FFI_FLAG}} picostream/main.py + uv run python tools/build_gui.py diff --git a/tools/build_gui.py b/tools/build_gui.py new file mode 100644 index 0000000..e912d8d --- /dev/null +++ b/tools/build_gui.py @@ -0,0 +1,100 @@ +import glob +import os +import platform +import sys + +try: + import PyInstaller.__main__ +except ImportError: + print("Error: PyInstaller is not installed. Please run 'uv pip install pyinstaller'.") + sys.exit(1) + + +def find_libffi_dll() -> str | None: + """ + Find the libffi-*.dll file required for _ctypes on Windows. + + Searches in common locations: + 1. The active Python environment's DLLs directory. + 2. The base Python installation's DLLs directory (if in a venv). + 3. A Conda environment's Library/bin directory. + """ + if sys.platform != "win32": + return None + + search_paths = [] + # 1. Active environment's DLLs directory (sys.prefix is the venv path) + search_paths.append(os.path.join(sys.prefix, "DLLs")) + + # 2. Base Python installation's DLLs directory (if in a venv) + if hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix: + search_paths.append(os.path.join(sys.base_prefix, "DLLs")) + + # 3. Conda environment's Library/bin directory + conda_prefix = os.environ.get("CONDA_PREFIX") + if conda_prefix: + search_paths.append(os.path.join(conda_prefix, "Library", "bin")) + + print("--- Searching for libffi DLL on Windows ---") + unique_paths = sorted(list(set(p for p in search_paths if os.path.isdir(p)))) + + for path in unique_paths: + print(f" - Checking: {path}") + dll_pattern = os.path.join(path, "libffi-*.dll") + found_dlls = glob.glob(dll_pattern) + if found_dlls: + dll_path = found_dlls[0] + print(f" - Found: {dll_path}") + return dll_path + + return None + + +def main() -> None: + """ + Build the PicoStream GUI executable using PyInstaller. + """ + print(f"--- Starting PicoStream Build ---") + print(f"Python: {sys.version} ({platform.architecture()[0]}) on {sys.platform}") + + entry_script = "picostream/main.py" + app_name = "PicoStream" + + pyinstaller_args = [ + "--onefile", + "--windowed", + f"--name={app_name}", + ] + + if sys.platform == "win32": + libffi_path = find_libffi_dll() + if libffi_path: + # The format for --add-binary is "source;destination" on Windows. + # We add the DLL to the root of the bundle. + pyinstaller_args.append(f"--add-binary={libffi_path}{os.pathsep}.") + else: + print("\nERROR: Could not find libffi-*.dll on Windows.") + print("This DLL is required for the _ctypes module to work correctly.") + print("Searched in the following directories:") + for path in sorted(list(set(p for p in search_paths if os.path.isdir(p)))): + print(f" - {path}") + print("Please ensure you are using a standard CPython or Conda installation.") + sys.exit(1) + + # Add the entry script at the end + pyinstaller_args.append(entry_script) + + print("\n--- Running PyInstaller ---") + print(f"Arguments: {' '.join(pyinstaller_args)}") + + PyInstaller.__main__.run(pyinstaller_args) + + print(f"\n--- Build Complete ---") + if sys.platform == "win32": + print(f"Executable created at: dist\\{app_name}.exe") + else: + print(f"Executable created at: dist/{app_name}") + + +if __name__ == "__main__": + main() |
