# -*- coding: utf-8 -*- """ builder.py - Handles the execution of the PyInstaller build process in a separate thread. """ import subprocess import threading import queue import os import traceback # For logging exceptions def run_build_in_thread(command, working_dir, output_queue, logger_func, output_dir_name, environment=None): """ Executes the PyInstaller command using subprocess.Popen in the background. Designed to be run in a separate thread. Captures stdout/stderr and sends messages (LOG, BUILD_SUCCESS, BUILD_ERROR, BUILD_FINISHED) back to the main GUI thread via the provided queue. Args: command (list): The command list to execute (e.g., ['pyinstaller', ...]). working_dir (str): The directory where the command should be executed. output_queue (queue.Queue): The queue to send status messages back. logger_func (callable): Function for logging (takes message, level). output_dir_name (str): Expected output directory name (e.g., '_dist'). environment (dict, optional): Environment variables for the subprocess. Defaults to None (inherit/use system default). """ build_process = None # Define variable outside try block for broader scope if needed try: logger_func("Build thread starting execution...", level="INFO") # Log the environment being used if it's custom env_log_msg = f"Using {'custom' if environment else 'inherited'} environment." if environment: # Optionally log specific variables for debugging (be careful with sensitive data) # env_details = {k: v for k, v in environment.items() if 'TCL' in k or 'TK' in k or 'PATH' in k} # env_log_msg += f" Details (partial): {env_details}" pass # Avoid logging full env by default logger_func(f"PyInstaller process starting. {env_log_msg}", level="INFO") # Start the PyInstaller process build_process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # Redirect stderr to stdout cwd=working_dir, # Set working directory text=True, # Decode output as text encoding='utf-8', # Specify encoding errors='replace', # Handle decoding errors gracefully bufsize=1, # Line-buffered output env=environment # Pass the specific environment # Note: creationflags might be needed on Windows to hide console, # e.g., creationflags=subprocess.CREATE_NO_WINDOW ) logger_func(f"PyInstaller process launched (PID: {build_process.pid}).", level="INFO") # --- Read output line by line in real-time --- while True: try: line = build_process.stdout.readline() if not line: # Check if process ended before breaking the loop if build_process.poll() is not None: logger_func("End of PyInstaller output stream.", level="DEBUG") break else: # Process might still be running, briefly wait or continue loop pass # readline should block, so usually no need to sleep else: # Send the captured line to the GUI log via the queue output_queue.put(("LOG", line)) except Exception as read_err: # Log errors during stream reading logger_func(f"Error reading PyInstaller output stream: {read_err}\n{traceback.format_exc()}", level="ERROR") # Decide whether to break or continue based on the error break # Safer to break if reading fails # --- Wait for process completion and get exit code --- return_code = build_process.wait() logger_func(f"PyInstaller process finished with exit code: {return_code}", level="INFO") # --- Send final status message to the queue --- if return_code == 0: dist_path_abs = os.path.join(working_dir, output_dir_name) if os.path.exists(dist_path_abs): success_msg = f"Build completato con successo!\nDirectory Output: {dist_path_abs}" output_queue.put(("BUILD_SUCCESS", success_msg)) else: # Build reported success, but output dir is missing! warn_msg = (f"Build terminato con codice 0, ma directory output non trovata:\n" f"{dist_path_abs}\nControlla il log completo per eventuali problemi.") logger_func(warn_msg, level="WARNING") output_queue.put(("BUILD_ERROR", warn_msg)) # Report as error to user else: # Build failed error_msg = f"Build fallito! (Codice Uscita: {return_code})\nControlla il log per dettagli." output_queue.put(("BUILD_ERROR", error_msg)) except FileNotFoundError: # Handle case where the pyinstaller command itself isn't found error_msg = ("Errore: Comando 'pyinstaller' non trovato.\n" "Assicurati che PyInstaller sia installato correttamente e nel PATH.") output_queue.put(("BUILD_ERROR", error_msg)) logger_func(error_msg, level="CRITICAL") except Exception as e: # Handle any other unexpected exceptions during subprocess management error_msg = f"Errore inatteso durante l'esecuzione del processo di build: {e}" output_queue.put(("BUILD_ERROR", error_msg)) logger_func(error_msg, level="CRITICAL") logger_func(traceback.format_exc(), level="DEBUG") # Log full traceback for debugging finally: # --- Crucial: Always signal that the build finished --- # This allows the main thread to re-enable the build button etc. output_queue.put(("BUILD_FINISHED", None)) # Send finish signal logger_func("Build thread finished.", level="INFO")