442 lines
20 KiB
Python
442 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Manages the core PyInstaller options and their associated GUI widgets.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import pathlib
|
|
import tkinter as tk
|
|
from tkinter import filedialog, ttk, messagebox
|
|
from typing import Optional, Callable, Dict, Any, Tuple, List
|
|
|
|
from pyinstallerguiwrapper import config # Importa la configurazione globale
|
|
|
|
|
|
# Definizione di un logger di default per OptionsManager se non ne viene passato uno
|
|
def _default_logger(message: str, level: str = "INFO") -> None:
|
|
"""Default logger for OptionsManager if none is provided."""
|
|
# print(f"[{level}] OptionsManager: {message}")
|
|
pass
|
|
|
|
|
|
class OptionsManager:
|
|
"""
|
|
Manages the creation, display, and retrieval of core PyInstaller build options
|
|
(like app name, icon, onefile/onedir, windowed/console, UPX, clean build, and wexpect helper).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
parent_gui: tk.Tk, # Riferimento alla finestra principale per i dialoghi
|
|
options_frame: ttk.Frame, # Il frame (es. basic_options_frame) dove verranno posizionati i widget
|
|
icon_path_var: tk.StringVar, # StringVar per il path dell'icona (esterno, gestito anche da ProjectManager)
|
|
app_name_var: tk.StringVar, # StringVar per il nome dell'app
|
|
is_onefile_var: tk.BooleanVar, # BooleanVar per onefile/onedir
|
|
is_windowed_var: tk.BooleanVar, # BooleanVar per windowed/console
|
|
use_upx_var: tk.BooleanVar, # BooleanVar per UPX
|
|
clean_output_dir_var: tk.BooleanVar, # BooleanVar per clean output dir
|
|
build_wexpect_helper_var: tk.BooleanVar, # BooleanVar per wexpect helper
|
|
derived_icon_path: Optional[str], # Path dell'icona derivata da ProjectManager
|
|
auto_include_hiddenimports_var: Optional[tk.BooleanVar] = None, # New option: auto-include detected hiddenimports
|
|
logger_func: Optional[Callable[..., None]] = None,
|
|
):
|
|
"""
|
|
Initializes the OptionsManager with Tkinter variables and widget references.
|
|
"""
|
|
self.parent_gui = parent_gui
|
|
self.options_frame = options_frame
|
|
self.icon_path_var = icon_path_var
|
|
self.app_name_var = app_name_var
|
|
self.is_onefile_var = is_onefile_var
|
|
self.is_windowed_var = is_windowed_var
|
|
self.use_upx_var = use_upx_var
|
|
self.clean_output_dir_var = clean_output_dir_var
|
|
self.build_wexpect_helper_var = (
|
|
build_wexpect_helper_var # Correzione errore di battitura
|
|
)
|
|
# New option: auto-include detected hiddenimports
|
|
if auto_include_hiddenimports_var is None:
|
|
self.auto_include_hiddenimports_var = tk.BooleanVar(value=False)
|
|
else:
|
|
self.auto_include_hiddenimports_var = auto_include_hiddenimports_var
|
|
self.derived_icon_path = derived_icon_path # Viene da ProjectManager
|
|
self.logger = logger_func if logger_func else _default_logger
|
|
|
|
# Widgets (references to be stored after creation)
|
|
self.icon_entry: Optional[ttk.Entry] = None
|
|
self.derived_icon_label_val: Optional[ttk.Label] = (
|
|
None # Per aggiornare il testo dell'icona derivata
|
|
)
|
|
|
|
self._create_widgets() # Crea e posiziona i widget nel frame passato
|
|
|
|
def _log(self, message: str, level: str = "INFO") -> None:
|
|
"""Helper for logging messages with a consistent prefix."""
|
|
try:
|
|
self.logger(f"[OptionsManager] {message}", level=level)
|
|
except TypeError:
|
|
self.logger(f"[{level}][OptionsManager] {message}")
|
|
|
|
def _create_widgets(self) -> None:
|
|
"""Creates and lays out the widgets for the core PyInstaller options."""
|
|
# Main layout for options_frame (basic_options_frame)
|
|
self.options_frame.columnconfigure(1, weight=1) # Make entry expand
|
|
|
|
current_row = 0
|
|
ttk.Label(self.options_frame, text="App Name (from Spec/GUI):").grid(
|
|
row=current_row, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
ttk.Entry(self.options_frame, textvariable=self.app_name_var).grid(
|
|
row=current_row, column=1, columnspan=3, padx=5, pady=5, sticky="ew"
|
|
)
|
|
current_row += 1
|
|
|
|
ttk.Label(self.options_frame, text="Icon File (from Spec/GUI):").grid(
|
|
row=current_row, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.icon_entry = ttk.Entry(
|
|
self.options_frame,
|
|
textvariable=self.icon_path_var,
|
|
state="readonly",
|
|
width=70,
|
|
)
|
|
self.icon_entry.grid(row=current_row, column=1, padx=5, pady=5, sticky="ew")
|
|
ttk.Button(
|
|
self.options_frame, text="Browse...", command=self._select_icon_file
|
|
).grid(row=current_row, column=2, padx=(0, 2), pady=5)
|
|
ttk.Button(
|
|
self.options_frame, text="Clear", command=self._clear_icon_file
|
|
).grid(row=current_row, column=3, padx=(2, 5), pady=5)
|
|
current_row += 1
|
|
|
|
ttk.Checkbutton(
|
|
self.options_frame,
|
|
text="Single File (One Executable)",
|
|
variable=self.is_onefile_var,
|
|
).grid(row=current_row, column=0, columnspan=4, padx=5, pady=5, sticky="w")
|
|
current_row += 1
|
|
ttk.Checkbutton(
|
|
self.options_frame,
|
|
text="Windowed (No Console)",
|
|
variable=self.is_windowed_var,
|
|
).grid(row=current_row, column=0, columnspan=4, padx=5, pady=5, sticky="w")
|
|
current_row += 1
|
|
ttk.Checkbutton(
|
|
self.options_frame,
|
|
text="Use UPX (if available in PATH)",
|
|
variable=self.use_upx_var,
|
|
).grid(row=current_row, column=0, columnspan=4, padx=5, pady=5, sticky="w")
|
|
current_row += 1
|
|
ttk.Checkbutton(
|
|
self.options_frame,
|
|
text="Clean output directory before build (deletes existing '_dist' content)",
|
|
variable=self.clean_output_dir_var,
|
|
).grid(row=current_row, column=0, columnspan=4, padx=5, pady=5, sticky="w")
|
|
current_row += 1
|
|
|
|
# Wexpect Helper Option
|
|
wexpect_check = ttk.Checkbutton(
|
|
self.options_frame,
|
|
text="Build and include 'wexpect' console helper (Windows only)",
|
|
variable=self.build_wexpect_helper_var,
|
|
command=self._on_wexpect_helper_option_changed,
|
|
)
|
|
wexpect_check.grid(row=current_row, column=0, columnspan=4, padx=5, pady=2, sticky="w")
|
|
|
|
if sys.platform != "win32":
|
|
wexpect_check.config(state="disabled")
|
|
self.build_wexpect_helper_var.set(False)
|
|
|
|
current_row += 1
|
|
|
|
ttk.Label(
|
|
self.options_frame,
|
|
text="Needed if the target app uses 'wexpect' and fails to start subprocesses when frozen.\n"
|
|
"The main app should use an UNMODIFIED 'wexpect/host.py'.",
|
|
foreground="grey",
|
|
font=("TkDefaultFont", 8),
|
|
).grid(row=current_row, column=0, columnspan=4, padx=7, pady=(0, 5), sticky="w")
|
|
current_row += 1
|
|
|
|
# Auto-include detected hiddenimports option
|
|
ttk.Checkbutton(
|
|
self.options_frame,
|
|
text="Auto-include detected hiddenimports (may add third-party modules automatically)",
|
|
variable=self.auto_include_hiddenimports_var,
|
|
).grid(row=current_row, column=0, columnspan=4, padx=5, pady=5, sticky="w")
|
|
current_row += 1
|
|
|
|
def _select_icon_file(self) -> None:
|
|
"""Opens a file dialog to select the application icon."""
|
|
# This needs the project directory path from ProjectManager.
|
|
# We need a way to get this or pass it. For now, let's assume it's set in main_window.
|
|
# Or, we can expose a method in main_window to get the project_dir.
|
|
# Let's pass `project_directory_getter: Callable[[], str]` to init.
|
|
|
|
# Per semplificare la referenza, ora `parent_gui` ha `project_manager`
|
|
project_dir_str = self.parent_gui.project_manager.project_directory_path.get()
|
|
initial_dir = (
|
|
project_dir_str
|
|
if project_dir_str and os.path.isdir(project_dir_str)
|
|
else None
|
|
)
|
|
|
|
# Determine file types based on OS
|
|
filetypes, default_ext = self._get_icon_file_types_by_platform()
|
|
|
|
file_path = filedialog.askopenfilename(
|
|
title="Select Application Icon (Overrides Spec)",
|
|
filetypes=filetypes,
|
|
defaultextension=default_ext,
|
|
initialdir=initial_dir,
|
|
parent=self.parent_gui,
|
|
)
|
|
if file_path:
|
|
self.icon_path_var.set(file_path)
|
|
self._log(f"Icon selected manually: {file_path}", level="INFO")
|
|
|
|
def _clear_icon_file(self) -> None:
|
|
"""Clears the selected icon file path."""
|
|
self.icon_path_var.set("")
|
|
self._log("Icon selection cleared by user.", level="INFO")
|
|
|
|
# Questo label `derived_icon_label_val` deve essere passato da main_window se vogliamo aggiornarlo qui.
|
|
# Altrimenti, main_window dovrà riaggiornarlo dopo che la variabile è stata pulita.
|
|
# Per ora, è meglio che main_window gestisca l'aggiornamento dei suoi label derivati.
|
|
# Rimuovo l'accesso diretto a `self.derived_icon_label_val` da qui, perché non è un attributo di OptionsManager.
|
|
# main_window si occuperà di chiamare ProjectManager per aggiornare i label.
|
|
# self.derived_icon_label_val.config(text="Not found (optional)", foreground="grey")
|
|
|
|
def _get_icon_file_types_by_platform(self) -> Tuple[List[Tuple[str, str]], str]:
|
|
"""Helper to get icon file types based on the current platform."""
|
|
if sys.platform == "win32":
|
|
return config.ICON_FILE_TYPES_WINDOWS, config.ICON_DEFAULT_EXT_WINDOWS
|
|
elif sys.platform == "darwin":
|
|
return config.ICON_FILE_TYPES_MACOS, config.ICON_DEFAULT_EXT_MACOS
|
|
else: # Linux, etc.
|
|
return config.ICON_FILE_TYPES_LINUX, config.ICON_DEFAULT_EXT_LINUX
|
|
|
|
def _on_wexpect_helper_option_changed(self) -> None:
|
|
"""Callback when the wexpect helper checkbox state changes."""
|
|
if self.build_wexpect_helper_var.get():
|
|
self._log(
|
|
"Wexpect console helper build ENABLED. This will run an additional PyInstaller build for the helper.",
|
|
level="INFO",
|
|
)
|
|
messagebox.showinfo(
|
|
"Wexpect Helper Info",
|
|
"Building the 'wexpect console helper' is an advanced option.\n"
|
|
"It's needed if your target application uses 'wexpect' and fails to start subprocesses when frozen by PyInstaller.\n"
|
|
"Ensure the target application uses an UNMODIFIED 'wexpect/host.py'.\n"
|
|
"The helper will be named 'wexpect.exe' and placed in a 'wexpect' subdirectory of your main application's output (_dist/wexpect/wexpect.exe for onedir, or _MEIPASS/wexpect/wexpect.exe for onefile).",
|
|
parent=self.parent_gui,
|
|
)
|
|
else:
|
|
self._log("Wexpect console helper build DISABLED.", level="INFO")
|
|
|
|
# MODIFICHE INIZIO: Nuovo metodo per aggiornare il percorso dell'icona derivata
|
|
def update_derived_icon_path(self, new_path: Optional[str]) -> None:
|
|
"""Updates the internal derived icon path, typically from ProjectManager."""
|
|
self.derived_icon_path = new_path
|
|
self._log(f"Internal derived icon path updated to: {new_path}", level="DEBUG")
|
|
|
|
# MODIFICHE FINE
|
|
|
|
# MODIFICHE INIZIO: Nuovo metodo per resettare le opzioni ai valori predefiniti
|
|
def reset_to_defaults(
|
|
self,
|
|
derive_name: bool = True,
|
|
project_root_name: Optional[str] = None,
|
|
derived_icon_path: Optional[str] = None,
|
|
) -> None:
|
|
"""
|
|
Resets all Tkinter variables managed by this class to their default values.
|
|
|
|
Args:
|
|
derive_name (bool): If True, attempts to derive app name from project_root_name.
|
|
project_root_name (str, optional): The name of the project root, used if derive_name is True.
|
|
derived_icon_path (str, optional): The automatically derived icon path from ProjectManager.
|
|
"""
|
|
self._log("Resetting OptionsManager variables to defaults.", level="INFO")
|
|
|
|
# Reset app name
|
|
if derive_name and project_root_name:
|
|
self.app_name_var.set(project_root_name)
|
|
self._log(
|
|
f" App Name reset to derived project name: {project_root_name}",
|
|
level="DEBUG",
|
|
)
|
|
else:
|
|
self.app_name_var.set(
|
|
config.DEFAULT_SPEC_OPTIONS.get("name", "")
|
|
) # Default empty string
|
|
self._log(
|
|
" App Name reset to empty string (no derivation).", level="DEBUG"
|
|
)
|
|
|
|
# Reset icon path
|
|
# Usiamo il derived_icon_path passato per decidere se auto-popolare l'icona
|
|
if derived_icon_path and pathlib.Path(derived_icon_path).is_file():
|
|
self.icon_path_var.set(derived_icon_path)
|
|
self._log(
|
|
f" Icon path reset to derived: {derived_icon_path}", level="DEBUG"
|
|
)
|
|
else:
|
|
self.icon_path_var.set("") # Clear if no derived icon or it doesn't exist
|
|
self._log(" Icon path cleared.", level="DEBUG")
|
|
|
|
# Reset boolean options from config defaults
|
|
self.is_onefile_var.set(config.DEFAULT_SPEC_OPTIONS["onefile"])
|
|
self.is_windowed_var.set(config.DEFAULT_SPEC_OPTIONS["windowed"])
|
|
self.use_upx_var.set(config.DEFAULT_SPEC_OPTIONS["use_upx"])
|
|
self.clean_output_dir_var.set(config.DEFAULT_SPEC_OPTIONS["clean_build"])
|
|
self.build_wexpect_helper_var.set(
|
|
False
|
|
) # wexpect helper default should be False, as it's an advanced option
|
|
# Auto-include hiddenimports default
|
|
try:
|
|
self.auto_include_hiddenimports_var.set(False)
|
|
except Exception:
|
|
pass
|
|
|
|
self._log("OptionsManager variables reset.", level="INFO")
|
|
|
|
# MODIFICHE FINE
|
|
|
|
def get_options_for_spec(
|
|
self,
|
|
project_root_path_obj: pathlib.Path,
|
|
derived_main_script_path: Optional[str],
|
|
derived_source_dir_path: Optional[pathlib.Path],
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Collects the current GUI options from this manager into a dictionary
|
|
suitable for .spec file generation. Converts paths to be relative to project root.
|
|
"""
|
|
# App Name
|
|
app_name = self.app_name_var.get()
|
|
if (
|
|
not app_name and self.parent_gui.project_manager.project_root_name
|
|
): # Fallback to project root name
|
|
app_name = self.parent_gui.project_manager.project_root_name
|
|
|
|
# Icon Path (relative to project root)
|
|
icon_rel_path_for_spec: Optional[str] = None
|
|
icon_gui_path_str = self.icon_path_var.get()
|
|
if icon_gui_path_str and pathlib.Path(icon_gui_path_str).is_file():
|
|
try:
|
|
icon_rel_path_for_spec = os.path.relpath(
|
|
icon_gui_path_str, str(project_root_path_obj)
|
|
) # Usa project_root_path_obj passato
|
|
except ValueError:
|
|
self._log(
|
|
f"Cannot make icon path '{icon_gui_path_str}' relative to project root. Using absolute.",
|
|
level="WARNING",
|
|
)
|
|
icon_rel_path_for_spec = (
|
|
icon_gui_path_str # Use absolute if relative fails
|
|
)
|
|
|
|
# Script Path (relative to project root) - This is mostly handled by ProjectManager now
|
|
# but needed for completeness here for the spec
|
|
script_rel_path: Optional[str] = None
|
|
if (
|
|
derived_main_script_path
|
|
and pathlib.Path(derived_main_script_path).is_file()
|
|
):
|
|
try:
|
|
script_rel_path = os.path.relpath(
|
|
derived_main_script_path, str(project_root_path_obj)
|
|
) # Usa project_root_path_obj passato
|
|
except ValueError:
|
|
self._log(
|
|
f"Cannot make main script path '{derived_main_script_path}' relative to project root. Using absolute.",
|
|
level="WARNING",
|
|
)
|
|
script_rel_path = derived_main_script_path
|
|
|
|
# Pathex (source directory relative to project root)
|
|
source_dir_rel_path: str = "." # Default to project root for pathex
|
|
if derived_source_dir_path and derived_source_dir_path.is_dir():
|
|
try:
|
|
source_dir_rel_path = os.path.relpath(
|
|
str(derived_source_dir_path), str(project_root_path_obj)
|
|
) # Usa project_root_path_obj passato
|
|
except ValueError:
|
|
self._log(
|
|
f"Cannot make source dir path '{derived_source_dir_path}' relative. Using absolute.",
|
|
level="WARNING",
|
|
)
|
|
source_dir_rel_path = str(derived_source_dir_path)
|
|
|
|
return {
|
|
"app_name": app_name,
|
|
"icon_rel_path": icon_rel_path_for_spec,
|
|
"is_onefile": self.is_onefile_var.get(),
|
|
"is_windowed": self.is_windowed_var.get(),
|
|
"use_upx": self.use_upx_var.get(),
|
|
"clean_output_dir": self.clean_output_dir_var.get(), # Not a spec option, but needed for build orchestration
|
|
"build_wexect_helper": self.build_wexpect_helper_var.get(), # Not a spec option, but needed for build orchestration
|
|
# These are for spec generation, might be redundant with SpecTransformer's defaults
|
|
# but collected for clarity. SpecTransformer will use its own internal logic.
|
|
"analysis_scripts": [script_rel_path] if script_rel_path else [],
|
|
"analysis_pathex": [source_dir_rel_path] if source_dir_rel_path else ["."],
|
|
"pyz_cipher_var_name": "block_cipher",
|
|
"a_var_name": "a",
|
|
"pyz_var_name": "pyz",
|
|
"auto_include_hiddenimports": self.auto_include_hiddenimports_var.get(),
|
|
}
|
|
|
|
def load_options_from_parsed_spec(
|
|
self, parsed_options: Dict[str, Any], project_root_dir_str: str
|
|
) -> None:
|
|
"""
|
|
Updates the Tkinter variables with values parsed from an existing .spec file.
|
|
"""
|
|
self._log("Updating GUI from loaded/parsed spec options.", level="INFO")
|
|
|
|
if "name" in parsed_options and parsed_options["name"] is not None:
|
|
self.app_name_var.set(parsed_options["name"])
|
|
self._log(f" App Name from spec: {parsed_options['name']}", level="DEBUG")
|
|
|
|
if "icon" in parsed_options and parsed_options["icon"] is not None:
|
|
icon_spec_path_str = parsed_options["icon"]
|
|
abs_icon_path = pathlib.Path(project_root_dir_str) / icon_spec_path_str
|
|
if abs_icon_path.exists():
|
|
self.icon_path_var.set(str(abs_icon_path))
|
|
self._log(
|
|
f" Resolved relative spec icon path: '{icon_spec_path_str}' -> '{abs_icon_path}'",
|
|
level="DEBUG",
|
|
)
|
|
else:
|
|
self._log(
|
|
f" Icon from spec ('{abs_icon_path}') not found. GUI icon field unchanged.",
|
|
level="WARNING",
|
|
)
|
|
|
|
if "onefile" in parsed_options and isinstance(
|
|
parsed_options.get("onefile"), bool
|
|
):
|
|
self.is_onefile_var.set(parsed_options["onefile"])
|
|
self._log(
|
|
f" OneFile from spec: {parsed_options['onefile']}", level="DEBUG"
|
|
)
|
|
|
|
if "windowed" in parsed_options and isinstance(
|
|
parsed_options.get("windowed"), bool
|
|
):
|
|
self.is_windowed_var.set(parsed_options["windowed"])
|
|
self._log(
|
|
f" Windowed (no console) from spec: {parsed_options['windowed']}",
|
|
level="DEBUG",
|
|
)
|
|
|
|
if "upx" in parsed_options and isinstance(parsed_options.get("upx"), bool):
|
|
self.use_upx_var.set(parsed_options["upx"])
|
|
self._log(f" Use UPX from spec: {parsed_options['upx']}", level="DEBUG")
|
|
|
|
# clean_output_dir and build_wexect_helper are GUI-only options, not loaded from spec.
|
|
|
|
self._log("Core GUI options updated from spec.", level="INFO")
|