SXXXXXXX_PyInstallerGUIWrapper/pyinstallerguiwrapper/gui.py

1057 lines
59 KiB
Python

# -*- coding: utf-8 -*-
""" gui.py - Main GUI module for the PyInstaller Wrapper. """
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox
from tkinter import scrolledtext
import tkinter.simpledialog as simpledialog
import os
import sys
import threading
import queue
import traceback
import shutil
import pathlib
import subprocess # Needed for git commands
import datetime # Needed for build timestamp
import re # Needed for function generation (tag extraction, placeholder matching)
# --- Import Version Info FOR THE WRAPPER ITSELF ---
try:
# Use absolute import based on package name
from pyinstallerguiwrapper import _version as wrapper_version
WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})"
WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
except ImportError:
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
# --- End Import Version Info ---
# Import other modules from the package (using absolute imports)
from pyinstallerguiwrapper import config
from pyinstallerguiwrapper import spec_parser
from pyinstallerguiwrapper import builder
# --- Constants for Version Generation ---
DEFAULT_VERSION = "0.0.0+unknown"
DEFAULT_COMMIT = "Unknown"
DEFAULT_BRANCH = "Unknown"
# --- End Constants ---
class PyInstallerGUI(tk.Tk):
""" Main application window for the PyInstaller GUI tool. """
def __init__(self):
""" Initialize the main application window, variables, and widgets. """
super().__init__()
self.title(f"PyInstaller GUI Wrapper - {WRAPPER_APP_VERSION_STRING}")
# --- Project Path ---
self.project_directory_path = tk.StringVar()
# --- Derived Paths (Internal state, might be displayed) ---
self._derived_spec_path = ""
self._derived_icon_path = ""
self._derived_main_script_path = ""
self._derived_source_dir_path = None
self._project_root_name = ""
self._backup_performed = False # Nuovo flag per tracciare il backup
self._temp_backup_dir_path = None # Nuovo per memorizzare il percorso del backup
# --- Tkinter Variables ---
self.icon_path = tk.StringVar()
self.app_name = tk.StringVar()
self.is_onefile = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['onefile'])
self.is_windowed = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['windowed'])
self.log_level = tk.StringVar(value=config.DEFAULT_SPEC_OPTIONS['log_level'])
# MODIFIED: Added BooleanVar for the clean output directory option
self.clean_output_dir = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['clean_build'])
# --- Data Structures for Complex Options ---
self.added_data_list = []
# --- Build Process State ---
self.build_thread = None
self.build_queue = queue.Queue()
# --- Initialize GUI ---
self.main_frame = ttk.Frame(self, padding="10")
self.main_frame.grid(row=0, column=0, sticky="nsew")
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self._create_widgets(self.main_frame)
self._layout_widgets()
self.after(100, self._check_build_queue)
self._log_to_gui(f"Running {WRAPPER_APP_VERSION_STRING}", level="INFO")
self._log_to_gui(f"{WRAPPER_BUILD_INFO}", level="DEBUG")
self._log_to_gui("Application initialized. Select the project directory.", level="INFO")
def _run_git_command_in_dir(self, command, target_dir):
""" Executes a Git command in a specific directory. """
self._log_to_gui(f"Running Git command in '{target_dir}': {command}", level="DEBUG")
try:
process = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
cwd=target_dir,
check=False,
errors='ignore'
)
if process.returncode == 0:
return process.stdout.strip()
else:
self._log_to_gui(f"Git command failed (rc={process.returncode}) in '{target_dir}': '{command}'. Stderr: {process.stderr.strip()}", level="WARNING")
return None
except FileNotFoundError:
self._log_to_gui("Git command failed: 'git' not found. Is Git installed and in PATH?", level="ERROR")
return None
except Exception as e:
self._log_to_gui(f"Error running git command '{command}' in '{target_dir}': {e}", level="ERROR")
self._log_to_gui(traceback.format_exc(), level="DEBUG")
return None
def _generate_target_version_file(self, project_dir, target_version_file_path):
"""
Checks if project_dir is a git repo, gathers version info,
and writes it to target_version_file_path, including a helper function.
Writes defaults if not a git repo or on error.
Returns True if file was written, False on critical write error.
"""
self._log_to_gui(f"Attempting to generate version file for target: {target_version_file_path}", level="INFO")
version_info = {
"version": DEFAULT_VERSION,
"commit": DEFAULT_COMMIT,
"branch": DEFAULT_BRANCH,
}
is_git_repo = False
git_check = self._run_git_command_in_dir("git rev-parse --is-inside-work-tree", project_dir)
if git_check == "true":
is_git_repo = True
self._log_to_gui(f"Project directory '{project_dir}' identified as a Git repository.", level="INFO")
describe_output = self._run_git_command_in_dir("git describe --tags --always --dirty", project_dir)
if describe_output:
version_info["version"] = describe_output
commit_output = self._run_git_command_in_dir("git rev-parse HEAD", project_dir)
if commit_output:
version_info["commit"] = commit_output
branch_output = self._run_git_command_in_dir("git rev-parse --abbrev-ref HEAD", project_dir)
if branch_output and branch_output != 'HEAD':
version_info["branch"] = branch_output
elif branch_output == 'HEAD' and version_info["commit"] != DEFAULT_COMMIT:
version_info["branch"] = f"detached@{version_info['commit'][:7]}"
else:
self._log_to_gui(f"Project directory '{project_dir}' is not a Git repository or Git failed. Using default version info.", level="WARNING")
timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
get_version_function_code = rf'''
import re
# --- Version Data (Generated) ---
# This section is automatically generated by the build process.
__version__ = "{version_info['version']}"
GIT_COMMIT_HASH = "{version_info['commit']}"
GIT_BRANCH = "{version_info['branch']}"
BUILD_TIMESTAMP = "{timestamp}"
IS_GIT_REPO = {is_git_repo}
# --- Default Values (for comparison or fallback) ---
DEFAULT_VERSION = "{DEFAULT_VERSION}"
DEFAULT_COMMIT = "{DEFAULT_COMMIT}"
DEFAULT_BRANCH = "{DEFAULT_BRANCH}"
# --- Helper Function ---
def get_version_string(format_string=None):
"""
Returns a formatted string based on the build version information.
Args:
format_string (str, optional): A format string using placeholders.
Defaults to "{{{{version}}}} ({{{{branch}}}}/{{{{commit_short}}}})" if None.
Placeholders:
{{{{version}}}}: Full version string (e.g., 'v1.0.0-5-gabcdef-dirty')
{{{{tag}}}}: Clean tag part if exists (e.g., 'v1.0.0'), else DEFAULT_VERSION.
{{{{commit}}}}: Full Git commit hash.
{{{{commit_short}}}}: Short Git commit hash (7 chars).
{{{{branch}}}}: Git branch name.
{{{{dirty}}}}: '-dirty' if the repo was dirty, empty otherwise.
{{{{timestamp}}}}: Full build timestamp (ISO 8601 UTC).
{{{{timestamp_short}}}}: Build date only (YYYY-MM-DD).
{{{{is_git}}}}: 'Git' if IS_GIT_REPO is True, 'Unknown' otherwise.
Returns:
str: The formatted version string, or an error message if formatting fails.
"""
if format_string is None:
format_string = "{{version}} ({{branch}}/{{commit_short}})"
replacements = {{}}
try:
replacements['version'] = __version__ if __version__ else DEFAULT_VERSION
replacements['commit'] = GIT_COMMIT_HASH if GIT_COMMIT_HASH else DEFAULT_COMMIT
replacements['commit_short'] = GIT_COMMIT_HASH[:7] if GIT_COMMIT_HASH and len(GIT_COMMIT_HASH) >= 7 else DEFAULT_COMMIT
replacements['branch'] = GIT_BRANCH if GIT_BRANCH else DEFAULT_BRANCH
replacements['timestamp'] = BUILD_TIMESTAMP if BUILD_TIMESTAMP else "Unknown"
replacements['timestamp_short'] = BUILD_TIMESTAMP.split('T')[0] if BUILD_TIMESTAMP and 'T' in BUILD_TIMESTAMP else "Unknown"
replacements['is_git'] = "Git" if IS_GIT_REPO else "Unknown"
replacements['dirty'] = "-dirty" if __version__ and __version__.endswith('-dirty') else ""
tag = DEFAULT_VERSION
if __version__ and IS_GIT_REPO:
match = re.match(r'^(v?([0-9]+)\.([0-9]+)\.([0-9]+))', __version__)
if match:
tag = match.group(1)
replacements['tag'] = tag
output_string = format_string
for placeholder, value in replacements.items():
pattern = re.compile(r'{{\s*' + re.escape(placeholder) + r'\s*}}')
output_string = pattern.sub(str(value), output_string)
if re.search(r'{{\s*[\w_]+\s*}}', output_string):
pass
return output_string
except Exception as e:
return f"[Formatting Error: {{e}}]"
'''
content = f'''\
# -*- coding: utf-8 -*-
# File generated by PyInstaller GUI Wrapper. DO NOT EDIT MANUALLY.
# Contains build-time information scraped from Git (if available)
# and a helper function to format version strings.
{get_version_function_code}
'''
try:
os.makedirs(os.path.dirname(target_version_file_path), exist_ok=True)
with open(target_version_file_path, 'w', encoding='utf-8') as f:
f.write(content)
self._log_to_gui(f"Successfully generated/updated target version file (with function): {target_version_file_path}", level="INFO")
if is_git_repo:
try:
default_output = self._get_default_formatted_version(version_info)
self._log_to_gui(f" -> Default format output: {default_output}", level="DEBUG")
except Exception as log_e:
self._log_to_gui(f" -> (Could not evaluate default format for logging: {log_e})", level="DEBUG")
return True
except Exception as e:
self._log_to_gui(f"CRITICAL ERROR: Failed to write target version file '{target_version_file_path}': {e}", level="CRITICAL")
self._log_to_gui(traceback.format_exc(), level="DEBUG")
messagebox.showerror("Version File Error", f"Could not write the version file for the target project:\n{target_version_file_path}\n\nError: {e}\n\nBuild cannot proceed reliably.")
return False
def _get_default_formatted_version(self, version_info):
""" Replicates the default format logic from the generated function for logging. """
v = version_info['version'] if version_info['version'] else DEFAULT_VERSION
b = version_info['branch'] if version_info['branch'] else DEFAULT_BRANCH
c = version_info['commit'][:7] if version_info['commit'] and len(version_info['commit']) >= 7 else DEFAULT_COMMIT
return f"{v} ({b}/{c})"
def _create_widgets(self, parent_frame):
# --- Section 1: Input Project Directory ---
self.project_frame = ttk.LabelFrame(parent_frame, text="Project Directory")
self.project_dir_label = ttk.Label(self.project_frame, text="Main Project Directory:")
self.project_dir_entry = ttk.Entry(self.project_frame, textvariable=self.project_directory_path, state="readonly", width=80)
self.project_dir_button_browse = ttk.Button(self.project_frame, text="Browse...", command=self._select_project_directory)
# --- Section 1.5: Derived Paths Display ---
self.derived_paths_frame = ttk.LabelFrame(parent_frame, text="Derived Paths (Automatic)")
self.derived_script_label_info = ttk.Label(self.derived_paths_frame, text="Script Found:")
self.derived_script_label_val = ttk.Label(self.derived_paths_frame, text="N/A", foreground="grey", anchor="w")
self.derived_spec_label_info = ttk.Label(self.derived_paths_frame, text="Spec File Found:")
self.derived_spec_label_val = ttk.Label(self.derived_paths_frame, text="N/A", foreground="grey", anchor="w")
self.derived_icon_label_info = ttk.Label(self.derived_paths_frame, text="Icon Found:")
self.derived_icon_label_val = ttk.Label(self.derived_paths_frame, text="N/A", foreground="grey", anchor="w")
# --- Section 2: Core Options (using Notebook for Tabs) ---
self.options_notebook = ttk.Notebook(parent_frame)
# --- Tab 1: Basic Options ---
self.basic_options_frame = ttk.Frame(self.options_notebook, padding="10")
self.options_notebook.add(self.basic_options_frame, text="Basic Options (from Spec/GUI)")
self.name_label = ttk.Label(self.basic_options_frame, text="App Name (from Spec/GUI):")
self.name_entry = ttk.Entry(self.basic_options_frame, textvariable=self.app_name)
self.icon_label = ttk.Label(self.basic_options_frame, text="Icon File (from Spec/GUI):")
self.icon_entry = ttk.Entry(self.basic_options_frame, textvariable=self.icon_path, state="readonly", width=70)
self.icon_button_browse = ttk.Button(self.basic_options_frame, text="Browse...", command=self._select_icon)
self.icon_button_clear = ttk.Button(self.basic_options_frame, text="Clear", command=self._clear_icon)
self.onefile_check = ttk.Checkbutton(self.basic_options_frame, text="Single File (One Executable)", variable=self.is_onefile)
self.windowed_check = ttk.Checkbutton(self.basic_options_frame, text="Windowed (No Console)", variable=self.is_windowed)
# MODIFIED: Added Checkbutton for cleaning output directory
self.clean_dir_check = ttk.Checkbutton(
self.basic_options_frame,
text="Clean output directory before build (deletes existing '_dist' content)", # Text updated for clarity
variable=self.clean_output_dir
)
# --- Tab 2: Additional Files ---
self.data_files_frame = ttk.Frame(self.options_notebook, padding="10")
self.options_notebook.add(self.data_files_frame, text="Additional Files (from Spec/GUI)")
self.data_list_label = ttk.Label(self.data_files_frame, text="Files/Folders to Include (Source Relative to Project Directory -> Destination in bundle):")
self.data_list_scrollbar_y = ttk.Scrollbar(self.data_files_frame, orient=tk.VERTICAL)
self.data_list_scrollbar_x = ttk.Scrollbar(self.data_files_frame, orient=tk.HORIZONTAL)
self.data_listbox = tk.Listbox(
self.data_files_frame, selectmode=tk.SINGLE,
yscrollcommand=self.data_list_scrollbar_y.set, xscrollcommand=self.data_list_scrollbar_x.set,
height=6, width=80 )
self.data_list_scrollbar_y.config(command=self.data_listbox.yview)
self.data_list_scrollbar_x.config(command=self.data_listbox.xview)
self.data_buttons_frame = ttk.Frame(self.data_files_frame)
self.data_button_add_file = ttk.Button(self.data_buttons_frame, text="Add File...", command=self._add_data_file)
self.data_button_add_dir = ttk.Button(self.data_buttons_frame, text="Add Folder...", command=self._add_data_directory)
self.data_button_remove = ttk.Button(self.data_buttons_frame, text="Remove Selected", command=self._remove_selected_data)
# --- Section 3: Build Output Log ---
self.output_frame = ttk.LabelFrame(parent_frame, text="Build Log")
self.output_text = scrolledtext.ScrolledText(self.output_frame, wrap=tk.WORD, height=15, state="disabled")
# --- Section 4: Action Buttons ---
self.action_frame = ttk.Frame(parent_frame)
self.build_button = ttk.Button(self.action_frame, text="Build Executable", command=self._start_build, state="disabled")
def _layout_widgets(self):
row_idx = 0
self.main_frame.rowconfigure(row_idx, weight=0)
row_idx += 1
self.main_frame.rowconfigure(row_idx, weight=0)
row_idx += 1
self.main_frame.rowconfigure(row_idx, weight=0)
row_idx += 1
self.main_frame.rowconfigure(row_idx, weight=1)
row_idx += 1
self.main_frame.rowconfigure(row_idx, weight=0)
self.main_frame.columnconfigure(0, weight=1)
row_idx = 0
self.project_frame.grid(row=row_idx, column=0, padx=5, pady=(0, 5), sticky="ew")
self.project_frame.columnconfigure(1, weight=1)
self.project_dir_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.project_dir_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
self.project_dir_button_browse.grid(row=0, column=2, padx=5, pady=5)
row_idx += 1
self.derived_paths_frame.grid(row=row_idx, column=0, padx=5, pady=5, sticky="ew")
self.derived_paths_frame.columnconfigure(1, weight=1)
self.derived_script_label_info.grid(row=0, column=0, padx=5, pady=2, sticky="w")
self.derived_script_label_val.grid(row=0, column=1, padx=5, pady=2, sticky="ew")
self.derived_spec_label_info.grid(row=1, column=0, padx=5, pady=2, sticky="w")
self.derived_spec_label_val.grid(row=1, column=1, padx=5, pady=2, sticky="ew")
self.derived_icon_label_info.grid(row=2, column=0, padx=5, pady=2, sticky="w")
self.derived_icon_label_val.grid(row=2, column=1, padx=5, pady=2, sticky="ew")
row_idx += 1
self.options_notebook.grid(row=row_idx, column=0, padx=5, pady=5, sticky="ew")
self.basic_options_frame.columnconfigure(1, weight=1)
current_basic_opt_row = 0
self.name_label.grid(row=current_basic_opt_row, column=0, padx=5, pady=5, sticky="w")
self.name_entry.grid(row=current_basic_opt_row, column=1, columnspan=3, padx=5, pady=5, sticky="ew")
current_basic_opt_row += 1
self.icon_label.grid(row=current_basic_opt_row, column=0, padx=5, pady=5, sticky="w")
self.icon_entry.grid(row=current_basic_opt_row, column=1, padx=5, pady=5, sticky="ew")
self.icon_button_browse.grid(row=current_basic_opt_row, column=2, padx=(0, 2), pady=5)
self.icon_button_clear.grid(row=current_basic_opt_row, column=3, padx=(2, 5), pady=5)
current_basic_opt_row += 1
self.onefile_check.grid(row=current_basic_opt_row, column=0, columnspan=4, padx=5, pady=5, sticky="w")
current_basic_opt_row += 1
self.windowed_check.grid(row=current_basic_opt_row, column=0, columnspan=4, padx=5, pady=5, sticky="w")
current_basic_opt_row += 1
# MODIFIED: Added layout for the new Checkbutton
self.clean_dir_check.grid(row=current_basic_opt_row, column=0, columnspan=4, padx=5, pady=5, sticky="w")
self.data_files_frame.columnconfigure(0, weight=1)
self.data_files_frame.rowconfigure(1, weight=1)
self.data_list_label.grid(row=0, column=0, columnspan=2, padx=5, pady=(0, 5), sticky="w")
self.data_listbox.grid(row=1, column=0, padx=(5, 0), pady=5, sticky="nsew")
self.data_list_scrollbar_y.grid(row=1, column=1, padx=(0, 5), pady=5, sticky="ns")
self.data_list_scrollbar_x.grid(row=2, column=0, padx=5, pady=(0, 5), sticky="ew")
self.data_buttons_frame.grid(row=1, column=2, rowspan=2, padx=5, pady=5, sticky="ns")
self.data_button_add_file.pack(pady=2, fill=tk.X)
self.data_button_add_dir.pack(pady=2, fill=tk.X)
self.data_button_remove.pack(pady=2, fill=tk.X)
row_idx += 1
self.output_frame.grid(row=row_idx, column=0, padx=5, pady=5, sticky="nsew")
self.output_frame.rowconfigure(0, weight=1); self.output_frame.columnconfigure(0, weight=1)
self.output_text.grid(row=0, column=0, padx=5, pady=5, sticky="nsew")
row_idx += 1
self.action_frame.grid(row=row_idx, column=0, padx=5, pady=(5, 0), sticky="e")
self.build_button.pack(pady=5)
def _select_project_directory(self):
self._log_to_gui("="*20 + " PROJECT DIRECTORY SELECTION STARTED " + "="*20, level="INFO")
dir_path = filedialog.askdirectory(title="Select Main Project Directory")
if not dir_path:
self._log_to_gui("Directory selection cancelled.", level="INFO")
return
self.project_directory_path.set(dir_path)
self._log_to_gui(f"Selected project directory: {dir_path}", level="INFO")
project_root = pathlib.Path(dir_path)
self._project_root_name = project_root.name
project_name_lower = self._project_root_name.lower()
self._derived_source_dir_path = project_root / project_name_lower
self._derived_main_script_path = self._derived_source_dir_path / "__main__.py"
self._derived_spec_path = project_root / f"{project_name_lower}.spec"
self._derived_icon_path = project_root / f"{project_name_lower}.ico" # Default to .ico
# Adjust icon extension based on platform if a platform-specific default is preferred
# This part is more about the *expected* icon, user can always override
if sys.platform == "darwin":
self._derived_icon_path = project_root / f"{project_name_lower}.icns"
elif sys.platform != "win32": # Linux and other non-Windows/non-macOS
self._derived_icon_path = project_root / f"{project_name_lower}.png"
self._log_to_gui(f"Derived project name: {self._project_root_name}", level="DEBUG")
self._log_to_gui(f" Expected source path: {self._derived_source_dir_path}", level="DEBUG")
self._log_to_gui(f" Expected main script: {self._derived_main_script_path}", level="DEBUG")
self._log_to_gui(f" Expected spec file: {self._derived_spec_path}", level="DEBUG")
self._log_to_gui(f" Expected icon file: {self._derived_icon_path}", level="DEBUG")
valid_structure = True
missing_files = []
if self._derived_main_script_path.is_file():
self.derived_script_label_val.config(text=str(self._derived_main_script_path), foreground="black")
else:
self.derived_script_label_val.config(text="NOT FOUND!", foreground="red")
missing_files.append(f"Main script ({self._derived_main_script_path.name})")
valid_structure = False
if self._derived_spec_path.is_file():
self.derived_spec_label_val.config(text=str(self._derived_spec_path), foreground="black")
else:
self.derived_spec_label_val.config(text="NOT FOUND (will be generated)", foreground="orange")
# Not finding a spec is not a fatal error for proceeding, as we can generate it.
# valid_structure = False # Do not set to false, spec can be generated.
if self._derived_icon_path.is_file():
self.derived_icon_label_val.config(text=str(self._derived_icon_path), foreground="black")
self.icon_path.set(str(self._derived_icon_path))
else:
self.derived_icon_label_val.config(text="Not found (optional)", foreground="grey")
self.icon_path.set("")
if not self._derived_source_dir_path.is_dir():
self._log_to_gui(f"Warning: Expected source directory '{self._derived_source_dir_path.name}' does not exist.", level="WARNING")
# Reset options. If spec exists, _handle_existing_spec_file will update them.
self._reset_options_to_defaults(derive_name=True)
if self._derived_spec_path.is_file():
self._handle_existing_spec_file(str(self._derived_spec_path))
else:
self._log_to_gui(f"Spec file '{self._derived_spec_path.name}' not found. A new one will be generated if build proceeds.", level="WARNING")
self.app_name.set(self._project_root_name) # Default app name if no spec
# Enable build button if main script exists (spec can be generated)
if self._derived_main_script_path.is_file():
self.build_button.config(state="normal")
self._log_to_gui("Project selection complete. Review options or click 'Build Executable'.", level="INFO")
if not self._derived_spec_path.is_file():
self._log_to_gui(f"Note: Spec file '{self._derived_spec_path.name}' will be generated.", level="INFO")
else:
self.build_button.config(state="disabled")
error_msg = "Error: Main script not found in the expected location:\n" + str(self._derived_main_script_path)
self._log_to_gui(error_msg, level="ERROR")
messagebox.showerror("Invalid Project Structure", error_msg + "\n\nCannot proceed with build.")
def _handle_existing_spec_file(self, spec_path):
self._log_to_gui("-" * 15 + " Parsing Existing Spec File " + "-" * 15, level="INFO")
self._log_to_gui(f"Attempting to parse spec file: {spec_path}", level="INFO")
parsed_options = spec_parser.parse_spec_file(spec_path, logger_func=self._log_to_gui)
if parsed_options is not None:
if parsed_options:
self._update_gui_from_options(parsed_options, self.project_directory_path.get())
messagebox.showinfo("Spec File Loaded", f"Loaded options from:\n{spec_path}\nGUI fields updated.")
else:
messagebox.showwarning("Spec File Parsed - Options Not Recognized", f"Parsed spec file:\n{spec_path}\n\nNo common options recognized.\nBuild will use GUI settings and may overwrite the spec if changes are made.")
else:
messagebox.showerror("Spec File Parsing Error", f"Failed to parse spec file:\n{spec_path}\n\nCheck log. Proceeding will use GUI settings and may overwrite the spec.")
def _reset_options_to_defaults(self, derive_name=True):
self._log_to_gui("Resetting options to default values.", level="INFO")
default_app_name = ""
if derive_name and self._project_root_name:
default_app_name = self._project_root_name
self.app_name.set(default_app_name or config.DEFAULT_SPEC_OPTIONS['name'])
# Icon path is usually derived or set by user, clearing it here might be aggressive
# self.icon_path.set(config.DEFAULT_SPEC_OPTIONS['icon']) # Or keep current/derived
self.is_onefile.set(config.DEFAULT_SPEC_OPTIONS['onefile'])
self.is_windowed.set(config.DEFAULT_SPEC_OPTIONS['windowed'])
self.log_level.set(config.DEFAULT_SPEC_OPTIONS['log_level'])
# MODIFIED: Reset the clean output directory checkbox
self.clean_output_dir.set(config.DEFAULT_SPEC_OPTIONS['clean_build'])
self.added_data_list.clear()
self.data_listbox.delete(0, tk.END)
self._log_to_gui("Additional files list reset.", level="DEBUG")
def _update_gui_from_options(self, options, project_root_dir):
self._log_to_gui("Updating GUI from loaded/parsed spec options.", level="INFO")
if 'name' in options and options['name'] is not None: self.app_name.set(options['name'])
if 'icon' in options and options['icon'] is not None:
icon_spec_path = pathlib.Path(options['icon'])
abs_icon_path = icon_spec_path
if not icon_spec_path.is_absolute():
# If project_root_dir is where the spec file is, this is correct.
# If spec file can be elsewhere, this might need adjustment.
# Assuming spec is at project_root_dir for now.
abs_icon_path = pathlib.Path(project_root_dir) / icon_spec_path
self._log_to_gui(f" Resolved relative spec icon path: '{options['icon']}' -> '{abs_icon_path}'", level="DEBUG")
if abs_icon_path.exists():
self.icon_path.set(str(abs_icon_path))
else:
self._log_to_gui(f" Icon from spec '{abs_icon_path}' not found, GUI icon field unchanged.", level="WARNING")
if 'onefile' in options and isinstance(options.get('onefile'), bool): self.is_onefile.set(options['onefile'])
if 'windowed' in options and isinstance(options.get('windowed'), bool): self.is_windowed.set(options['windowed'])
# Note: 'clean_build' is not typically in a spec file, so it's not updated from 'options'.
self.added_data_list.clear()
self.data_listbox.delete(0, tk.END)
parsed_datas = options.get('datas', [])
if parsed_datas:
self._log_to_gui(f"Found 'datas' in spec: {len(parsed_datas)} entries.", level="INFO")
project_root_path = pathlib.Path(project_root_dir)
for item in parsed_datas:
if isinstance(item, (tuple, list)) and len(item) == 2:
source_rel_to_spec, destination_in_bundle = item # source is relative to where spec is
if isinstance(source_rel_to_spec, str) and isinstance(destination_in_bundle, str):
# We store source_rel_to_spec as provided by the spec file.
# When generating a new spec, paths in added_data_list are assumed relative to project_root.
# This assumes the spec file is at the project_root.
self.added_data_list.append((source_rel_to_spec, destination_in_bundle))
display_text = f"{source_rel_to_spec} -> {destination_in_bundle}"
self.data_listbox.insert(tk.END, display_text)
abs_source_check = project_root_path / source_rel_to_spec # For logging/verification
self._log_to_gui(f" Added data entry from spec: '{source_rel_to_spec}' (abs check: {abs_source_check}) -> '{destination_in_bundle}'", level="DEBUG")
else:
self._log_to_gui(f" Ignored invalid 'datas' entry (non-strings): {item}", level="WARNING")
else:
self._log_to_gui(f" Ignored invalid 'datas' entry (format not tuple/list of 2): {item}", level="WARNING")
self._log_to_gui("Additional files list populated from spec.", level="DEBUG")
else:
self._log_to_gui("No valid 'datas' entries found in spec.", level="DEBUG")
self._log_to_gui("GUI update from options completed.", level="INFO")
def _select_icon(self):
project_dir = self.project_directory_path.get()
initial_dir = project_dir if project_dir else None
if sys.platform == "win32": filetypes, default_ext = config.ICON_FILE_TYPES_WINDOWS, config.ICON_DEFAULT_EXT_WINDOWS
elif sys.platform == "darwin": filetypes, default_ext = config.ICON_FILE_TYPES_MACOS, config.ICON_DEFAULT_EXT_MACOS
else: filetypes, default_ext = config.ICON_FILE_TYPES_LINUX, config.ICON_DEFAULT_EXT_LINUX
file_path = filedialog.askopenfilename(title="Select Application Icon (Overrides)",
filetypes=filetypes,
defaultextension=default_ext,
initialdir=initial_dir)
if file_path:
self.icon_path.set(file_path)
self._log_to_gui(f"Icon selected manually (overrides): {file_path}", level="INFO")
def _clear_icon(self):
self.icon_path.set("")
self._log_to_gui("Icon selection cleared.")
if self._derived_icon_path and pathlib.Path(self._derived_icon_path).is_file():
self.derived_icon_label_val.config(text=str(self._derived_icon_path), foreground="black")
else:
self.derived_icon_label_val.config(text="Not found (optional)", foreground="grey")
def _add_data_file(self):
project_dir = self.project_directory_path.get()
if not project_dir:
messagebox.showerror("Error", "Select the project directory first."); return
source_path = filedialog.askopenfilename(title="Select Source File to Add", initialdir=project_dir)
if not source_path:
self._log_to_gui("Add file cancelled.", level="DEBUG"); return
try:
abs_source_path = pathlib.Path(source_path).resolve()
project_root_path = pathlib.Path(project_dir).resolve()
relative_source_path = os.path.relpath(str(abs_source_path), str(project_root_path))
self._log_to_gui(f"Calculated relative source path for spec: {relative_source_path}", level="DEBUG")
except ValueError as e:
messagebox.showerror("Path Error", f"Cannot calculate relative path.\nEnsure the file is on the same drive as the project directory or in a subfolder.\nError: {e}")
self._log_to_gui(f"Error calculating relative path between '{source_path}' and '{project_dir}': {e}", level="ERROR")
return
except Exception as e:
messagebox.showerror("Error", f"Unexpected error processing path: {e}")
self._log_to_gui(f"Error processing path '{source_path}': {e}", level="ERROR")
return
dest_path = simpledialog.askstring("Destination in Bundle",
f"Relative destination path for:\n{abs_source_path.name}\n(e.g., '.', 'data', 'assets/file.txt')",
initialvalue=".")
if dest_path is None:
self._log_to_gui("Add file cancelled (destination).", level="DEBUG"); return
entry = (relative_source_path, dest_path) # Store path relative to project root
self.added_data_list.append(entry)
display_text = f"{relative_source_path} -> {dest_path}"
self.data_listbox.insert(tk.END, display_text)
self._log_to_gui(f"Added data file: {display_text} (Source relative to project dir)", level="INFO")
def _add_data_directory(self):
project_dir = self.project_directory_path.get()
if not project_dir:
messagebox.showerror("Error", "Select the project directory first."); return
source_path = filedialog.askdirectory(title="Select Source Folder to Add", initialdir=project_dir)
if not source_path:
self._log_to_gui("Add folder cancelled.", level="DEBUG"); return
try:
abs_source_path = pathlib.Path(source_path).resolve()
project_root_path = pathlib.Path(project_dir).resolve()
relative_source_path = os.path.relpath(str(abs_source_path), str(project_root_path))
self._log_to_gui(f"Calculated relative source path for spec: {relative_source_path}", level="DEBUG")
except ValueError as e:
messagebox.showerror("Path Error", f"Cannot calculate relative path.\nEnsure the folder is on the same drive as the project directory or in a subfolder.\nError: {e}")
self._log_to_gui(f"Error calculating relative path between '{source_path}' and '{project_dir}': {e}", level="ERROR")
return
except Exception as e:
messagebox.showerror("Error", f"Unexpected error processing path: {e}")
self._log_to_gui(f"Error processing path '{source_path}': {e}", level="ERROR")
return
default_dest = abs_source_path.name
dest_path = simpledialog.askstring("Destination in Bundle",
f"Relative destination path for folder:\n{default_dest}\n(e.g., '{default_dest}', 'data/{default_dest}')",
initialvalue=default_dest)
if dest_path is None:
self._log_to_gui("Add folder cancelled (destination).", level="DEBUG"); return
entry = (relative_source_path, dest_path) # Store path relative to project root
self.added_data_list.append(entry)
display_text = f"{relative_source_path} -> {dest_path}"
self.data_listbox.insert(tk.END, display_text)
self._log_to_gui(f"Added data folder: {display_text} (Source relative to project dir)", level="INFO")
def _remove_selected_data(self):
selected_indices = self.data_listbox.curselection()
if not selected_indices:
messagebox.showwarning("No Selection", "Select an item to remove."); return
index = selected_indices[0]
item_text = self.data_listbox.get(index)
self.data_listbox.delete(index)
if index < len(self.added_data_list):
self.added_data_list.pop(index)
self._log_to_gui(f"Removed data entry: {item_text}", level="INFO")
else:
self._log_to_gui(f"Error: Index {index} out of range for internal data list ({len(self.added_data_list)} entries). Listbox item: {item_text}", level="ERROR")
def _log_to_gui(self, message, level="INFO"):
formatted_message = f"[{level}] {str(message).strip()}\n"
if threading.current_thread() is threading.main_thread():
self._update_output_log_widget(formatted_message)
else:
self.build_queue.put(("LOG", formatted_message))
def _update_output_log_widget(self, message):
try:
self.output_text.config(state="normal")
self.output_text.insert(tk.END, message)
self.output_text.see(tk.END)
self.output_text.config(state="disabled")
self.output_text.update_idletasks()
except Exception as e:
print(f"[ERROR] Error updating log widget: {e}\n{traceback.format_exc()}")
def _check_build_queue(self):
try:
while True:
item = self.build_queue.get_nowait()
if isinstance(item, tuple) and len(item) == 2:
command_type, data = item # Rinominato 'command' in 'command_type' per chiarezza
if command_type == "LOG":
self._update_output_log_widget(data)
elif command_type == "BUILD_SUCCESS":
self._log_to_gui(data, level="SUCCESS") # data è il success_msg
messagebox.showinfo("Build Successful", data)
# --- MODIFICA INIZIO: Ripristino dei file .json e .ini ---
if self._backup_performed and self._temp_backup_dir_path and self._temp_backup_dir_path.exists():
self._log_to_gui("Attempting to restore backed-up config files...", level="INFO")
project_dir = self.project_directory_path.get()
dist_path_abs_str = os.path.join(project_dir, config.DEFAULT_SPEC_OPTIONS['output_dir_name'])
dist_path_abs = pathlib.Path(dist_path_abs_str)
if not dist_path_abs.is_dir():
self._log_to_gui(f"Output directory '{dist_path_abs_str}' not found after successful build. Cannot restore configs.", level="WARNING")
else:
restored_count = 0
try:
for src_file_path in self._temp_backup_dir_path.rglob('*'):
if src_file_path.is_file(): # Solo file, non cartelle vuote
relative_path = src_file_path.relative_to(self._temp_backup_dir_path)
dest_in_dist = dist_path_abs / relative_path
dest_in_dist.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_file_path, dest_in_dist)
self._log_to_gui(f" Restored: {src_file_path.name} -> {dest_in_dist}", level="DEBUG")
restored_count += 1
if restored_count > 0:
self._log_to_gui(f"Successfully restored {restored_count} config files.", level="INFO")
else:
self._log_to_gui("No files were found in the backup to restore (this is unexpected if backup was performed).", level="WARNING")
except Exception as e:
self._log_to_gui(f"Error during config restore: {e}", level="ERROR")
self._log_to_gui(traceback.format_exc(), level="DEBUG")
messagebox.showerror("Restore Error", f"Failed to restore configuration files: {e}")
elif self._backup_performed: # Backup era true ma il path non esiste/non è valido
self._log_to_gui("Backup was marked as performed, but backup directory is invalid. Cannot restore.", level="WARNING")
self._cleanup_backup_dir_if_exists() # Pulisci sempre dopo il tentativo di ripristino
# --- MODIFICA FINE: Ripristino ---
self._on_build_finished()
elif command_type == "BUILD_ERROR":
self._log_to_gui(data, level="ERROR")
messagebox.showerror("Build Failed", data)
self._cleanup_backup_dir_if_exists() # Pulisci anche in caso di errore di build
self._on_build_finished()
elif command_type == "BUILD_FINISHED":
self._log_to_gui("Build process finished signal received.", level="DEBUG")
# Non pulire qui, la pulizia avviene dopo BUILD_SUCCESS o BUILD_ERROR
self._on_build_finished()
else:
self._log_to_gui(f"Unknown command from queue: {command_type}", level="WARNING")
else:
self._log_to_gui(f"Unexpected item in queue: {item}", level="WARNING")
self.build_queue.task_done()
except queue.Empty:
pass
except Exception as e:
self._log_to_gui(f"Error processing build queue: {e}\n{traceback.format_exc()}", level="CRITICAL")
self._cleanup_backup_dir_if_exists() # Pulisci in caso di errore grave nella queue
finally:
self.after(100, self._check_build_queue)
# Modifica _on_build_finished per assicurarsi che non pulisca il backup
# se non è ancora stato gestito da BUILD_SUCCESS o BUILD_ERROR
def _on_build_finished(self):
project_dir = self.project_directory_path.get()
can_build_again = (project_dir and
self._derived_main_script_path and
pathlib.Path(self._derived_main_script_path).is_file())
self.build_button.config(state="normal" if can_build_again else "disabled")
self.build_thread = None
self._log_to_gui("Build process finished. GUI controls updated based on project state.", level="INFO")
# La pulizia del backup è ora gestita specificamente dopo BUILD_SUCCESS o BUILD_ERROR
# o se la build viene interrotta prima. Non dovrebbe essere necessario qui.
# self._cleanup_backup_dir_if_exists() # Rimosso da qui per evitare doppie pulizie o pulizie premature
def _generate_spec_content(self):
project_dir = self.project_directory_path.get()
if not project_dir:
messagebox.showerror("Error", "Project directory not selected."); return None
if not self._derived_main_script_path or not pathlib.Path(self._derived_main_script_path).is_file():
messagebox.showerror("Error", f"Main script '{self._derived_main_script_path}' not found or invalid."); return None
self._log_to_gui("Generating .spec content from GUI options...", level="INFO")
project_root_path = pathlib.Path(project_dir).resolve()
try:
script_rel_path = os.path.relpath(str(pathlib.Path(self._derived_main_script_path).resolve()), str(project_root_path))
except Exception as e:
self._log_to_gui(f"Cannot make script path relative '{self._derived_main_script_path}': {e}", level="ERROR")
messagebox.showerror("Path Error", f"Cannot calculate relative path for the main script:\n{e}")
return None
try:
# Ensure _derived_source_dir_path is a Path object as expected
if not isinstance(self._derived_source_dir_path, pathlib.Path):
# Fallback or error if it's not set correctly (should be from _select_project_directory)
self._log_to_gui(f"Source directory path is not initialized correctly. Using '.' for pathex.", level="WARNING")
source_dir_rel_path = "."
else:
source_dir_abs = self._derived_source_dir_path.resolve()
if not source_dir_abs.is_dir():
self._log_to_gui(f"Source directory '{source_dir_abs}' does not exist, using '.' for pathex.", level="WARNING")
source_dir_rel_path = "."
else:
source_dir_rel_path = os.path.relpath(str(source_dir_abs), str(project_root_path))
except Exception as e:
self._log_to_gui(f"Cannot make source dir path relative '{self._derived_source_dir_path}': {e}. Using '.' for pathex.", level="WARNING")
source_dir_rel_path = "."
app_name_val = self.app_name.get() or self._project_root_name
icon_gui_path = self.icon_path.get()
icon_rel_path = None
if icon_gui_path and pathlib.Path(icon_gui_path).is_file():
try:
icon_rel_path = os.path.relpath(str(pathlib.Path(icon_gui_path).resolve()), str(project_root_path))
except Exception as e:
self._log_to_gui(f"Cannot make icon path relative '{icon_gui_path}': {e}. Icon will not be included.", level="WARNING")
def escape_path(p): return p.replace("\\", "\\\\") if p else ""
analysis_scripts = f"['{escape_path(script_rel_path)}']"
analysis_pathex = f"['{escape_path(source_dir_rel_path)}']" # Paths relative to spec file location
# Ensure added_data_list paths are correctly relative to project_root_path (where spec will be)
formatted_datas = "[" + ",\n ".join([f"('{escape_path(src)}', '{escape_path(dst)}')" for src, dst in self.added_data_list]) + "]"
self._log_to_gui(f"Formatted {len(self.added_data_list)} 'datas' entries for spec (sources relative to project root).", level="DEBUG")
formatted_hiddenimports = "[]"; formatted_binaries = "[]" # TODO: Allow GUI input for these
analysis_str = f"""a = Analysis(scripts={analysis_scripts},
pathex={analysis_pathex},
binaries={formatted_binaries},
datas={formatted_datas},
hiddenimports={formatted_hiddenimports},
hookspath=[],
hooksconfig={{}},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False)"""
pyz_str = "pyz = PYZ(a.pure, a.zipped_data, cipher=None)"
exe_str = f"""exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='{app_name_val}',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx={config.DEFAULT_SPEC_OPTIONS['use_upx']},
runtime_tmpdir=None,
console={not self.is_windowed.get()},
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None"""
if icon_rel_path:
exe_str += f",\n icon='{escape_path(icon_rel_path)}'"
exe_str += ")"
collect_str = ""
if not self.is_onefile.get():
collect_str = f"""coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx={config.DEFAULT_SPEC_OPTIONS['use_upx']},
upx_exclude=[],
name='{app_name_val}')"""
spec_header = "# -*- mode: python ; coding: utf-8 -*-\n\nblock_cipher = None\n"
spec_imports = ""
if collect_str: # os may not be needed if a.datas is empty, but safer to include
spec_imports = "import os\n"
spec_content = f"{spec_header}\n{spec_imports}{analysis_str}\n\n{pyz_str}\n\n{exe_str}\n"
if collect_str:
spec_content += f"\n{collect_str}\n"
self._log_to_gui(".spec file content generated.", level="DEBUG")
return spec_content.strip() + "\n"
def _save_spec_file(self, content):
if not self._derived_spec_path:
messagebox.showerror("Error", "Cannot save spec: derived path not set or invalid."); return False
spec_path_to_save = str(self._derived_spec_path)
self._log_to_gui(f"Attempting to save spec file to: {spec_path_to_save}", level="INFO")
try:
os.makedirs(os.path.dirname(spec_path_to_save), exist_ok=True)
with open(spec_path_to_save, 'w', encoding='utf-8') as f:
f.write(content)
self._log_to_gui(f"Spec file saved successfully: {spec_path_to_save}", level="INFO")
self.derived_spec_label_val.config(text=spec_path_to_save, foreground="black") # Update label
return True
except Exception as e:
error_msg = f"Failed to save spec file '{spec_path_to_save}': {e}\n{traceback.format_exc()}"
self._log_to_gui(error_msg, level="ERROR"); messagebox.showerror("File Save Error", error_msg); return False
def _start_build(self):
self._log_to_gui("="*20 + " BUILD PROCESS STARTED " + "="*20, level="INFO")
project_dir = self.project_directory_path.get()
# --- VALIDAZIONI INIZIALI (come prima) ---
if not project_dir or not os.path.isdir(project_dir):
messagebox.showerror("Error", "Select a valid project directory."); self._log_to_gui("Build cancelled: invalid project directory.", level="ERROR"); return
# ... (altre validazioni come prima) ...
main_script_path_obj = pathlib.Path(self._derived_main_script_path)
if not main_script_path_obj.is_file():
messagebox.showerror("Error", f"Main script '{main_script_path_obj}' not found. Cannot proceed."); self._log_to_gui("Build cancelled: main script missing.", level="ERROR"); return
if not isinstance(self._derived_source_dir_path, pathlib.Path) or not self._derived_source_dir_path.is_dir():
messagebox.showerror("Error", f"Source directory '{self._derived_source_dir_path}' not found. Cannot generate version file."); self._log_to_gui("Build cancelled: source directory missing.", level="ERROR"); return
# --- RESET STATO BACKUP ---
self._backup_performed = False
self._temp_backup_dir_path = None
# --- MODIFICA INIZIO: Backup dei file .json e .ini ---
dist_path_abs_str = os.path.join(project_dir, config.DEFAULT_SPEC_OPTIONS['output_dir_name'])
dist_path_abs = pathlib.Path(dist_path_abs_str)
if not self.clean_output_dir.get() and dist_path_abs.is_dir():
self._log_to_gui(f"Backup process: 'Clean output directory' is OFF. Checking for .json/.ini files in '{dist_path_abs_str}'.", level="INFO")
# Creazione directory di backup temporanea
# Usiamo una sottocartella del progetto per semplicità, ma si potrebbe usare tempfile.mkdtemp()
self._temp_backup_dir_path = pathlib.Path(project_dir) / "_dist_config_backup_temp"
try:
if self._temp_backup_dir_path.exists():
shutil.rmtree(self._temp_backup_dir_path) # Pulisci backup precedente se esiste
self._temp_backup_dir_path.mkdir(parents=True, exist_ok=True)
self._log_to_gui(f"Created temporary backup directory: {self._temp_backup_dir_path}", level="DEBUG")
files_to_backup = []
for filepath in dist_path_abs.rglob('*'): # rglob per cercare ricorsivamente
if filepath.is_file() and filepath.suffix.lower() in ['.json', '.ini']:
files_to_backup.append(filepath)
if files_to_backup:
self._log_to_gui(f"Found {len(files_to_backup)} config files to backup.", level="INFO")
for src_file_path in files_to_backup:
# Calcola il percorso relativo del file rispetto a dist_path_abs
relative_path = src_file_path.relative_to(dist_path_abs)
# Crea il percorso di destinazione nel backup mantenendo la struttura
dest_in_backup = self._temp_backup_dir_path / relative_path
dest_in_backup.parent.mkdir(parents=True, exist_ok=True) # Assicura che la sottocartella esista
shutil.copy2(src_file_path, dest_in_backup) # copy2 preserva i metadati
self._log_to_gui(f" Backed up: {src_file_path} -> {dest_in_backup}", level="DEBUG")
self._backup_performed = True
else:
self._log_to_gui("No .json or .ini files found in existing _dist to backup.", level="INFO")
# Se non ci sono file, non c'è bisogno di tenere la cartella di backup
shutil.rmtree(self._temp_backup_dir_path)
self._temp_backup_dir_path = None
except Exception as e:
self._log_to_gui(f"Error during config backup: {e}", level="ERROR")
self._log_to_gui(traceback.format_exc(), level="DEBUG")
messagebox.showerror("Backup Error", f"Failed to backup configuration files: {e}")
# Non bloccare la build per un errore di backup, ma loggalo
self._backup_performed = False # Assicura che non si tenti un ripristino
if self._temp_backup_dir_path and self._temp_backup_dir_path.exists():
shutil.rmtree(self._temp_backup_dir_path) # Pulisci in caso di errore parziale
self._temp_backup_dir_path = None
elif self.clean_output_dir.get():
self._log_to_gui("'Clean output directory' is ON. Skipping config backup.", level="INFO")
elif not dist_path_abs.is_dir():
self._log_to_gui(f"Output directory '{dist_path_abs_str}' does not exist. Skipping config backup.", level="INFO")
# --- MODIFICA FINE: Backup ---
# --- GENERAZIONE _version.py (come prima) ---
target_version_file = self._derived_source_dir_path / "_version.py"
if not self._generate_target_version_file(project_dir, target_version_file):
self._log_to_gui("Build cancelled: Failed to generate target version file.", level="ERROR")
self._cleanup_backup_dir_if_exists() # Pulisci backup se la build fallisce qui
return
# --- GENERAZIONE E SALVATAGGIO SPEC FILE (come prima) ---
self._log_to_gui("Generating spec content from current GUI options...", level="INFO")
spec_content = self._generate_spec_content()
if spec_content is None:
self._log_to_gui("Build cancelled: spec generation failed.", level="ERROR")
self._cleanup_backup_dir_if_exists()
return
# ... (resto del codice per salvare lo spec file) ...
if not self._save_spec_file(spec_content):
self._log_to_gui("Build cancelled: spec save failed.", level="ERROR")
self._cleanup_backup_dir_if_exists()
return
# --- PREPARAZIONE COMANDO PYINSTALLER (come prima) ---
# ... (logica per preparare il comando, incluso il controllo di self.clean_output_dir.get() per --clean) ...
self._log_to_gui("Preparing PyInstaller execution...", level="INFO")
self.build_button.config(state="disabled") # Disable button during build
# working_dir = project_dir # Già definito
# dist_path_abs_str è già definito sopra
# work_path_abs = os.path.join(working_dir, config.DEFAULT_SPEC_OPTIONS['work_dir_name']) # work_path_abs è definito qui
work_path_abs_str = os.path.join(project_dir, config.DEFAULT_SPEC_OPTIONS['work_dir_name'])
pyinstaller_cmd_path = shutil.which("pyinstaller")
# ... (controllo pyinstaller_cmd_path) ...
if not pyinstaller_cmd_path:
error_msg = ("'pyinstaller' command not found in PATH.\n"
"Ensure it is installed and in your system's PATH.")
self._log_to_gui(error_msg, level="CRITICAL"); messagebox.showerror("PyInstaller Error", error_msg)
self._on_build_finished(); # _on_build_finished dovrebbe gestire la pulizia del backup
return
spec_path_to_save_str = str(self._derived_spec_path)
command = [
pyinstaller_cmd_path,
spec_path_to_save_str,
"--distpath", dist_path_abs_str, # Usa la stringa path qui
"--workpath", work_path_abs_str, # Usa la stringa path qui
"--log-level", self.log_level.get()
]
if self.clean_output_dir.get(): # Il flag --clean di PyInstaller
command.append("--clean")
self._log_to_gui("PyInstaller --clean flag will be used (due to GUI option).", level="INFO")
# else: # Non serve loggare di nuovo se non si usa, già fatto durante il backup check
# self._log_to_gui("PyInstaller --clean flag will NOT be used.", level="INFO")
if not config.DEFAULT_SPEC_OPTIONS['confirm_overwrite']:
command.append("--noconfirm")
# ... (resto della preparazione del comando e avvio del thread, come prima) ...
build_env = os.environ.copy()
# ... (rimozione TCL_LIBRARY, TK_LIBRARY) ...
quoted_command = [f'"{arg}"' if ' ' in arg else arg for arg in command]
self._log_to_gui(f"Execution Command: {' '.join(quoted_command)}", level="DEBUG")
self._log_to_gui(f"Working Directory: {project_dir}", level="DEBUG")
self.build_thread = threading.Thread(
target=builder.run_build_in_thread,
args=(
command,
project_dir,
self.build_queue,
self._log_to_gui,
config.DEFAULT_SPEC_OPTIONS['output_dir_name'],
build_env
),
daemon=True
)
self.build_thread.start()
self._log_to_gui("Build thread started.", level="INFO")
# Nuovo metodo helper per pulire la cartella di backup
def _cleanup_backup_dir_if_exists(self):
if self._temp_backup_dir_path and self._temp_backup_dir_path.exists():
try:
shutil.rmtree(self._temp_backup_dir_path)
self._log_to_gui(f"Cleaned up temporary backup directory: {self._temp_backup_dir_path}", level="DEBUG")
except Exception as e:
self._log_to_gui(f"Error cleaning up backup directory {self._temp_backup_dir_path}: {e}", level="WARNING")
self._temp_backup_dir_path = None
self._backup_performed = False
# --- End of PyInstallerGUI class ---