refactoring with new name

This commit is contained in:
VALLONGOL 2025-05-05 12:35:58 +02:00
parent 59109d3a18
commit c215604251
14 changed files with 1369 additions and 762 deletions

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Module",
"type": "debugpy",
"request": "launch",
"module": "pyinstallerguiwrapper"
}
]
}

View File

@ -1,16 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(scripts=['C:\\src\\____GitProjects\\CreateExecFromPy\\CreateExecFromPy.py'], pathex=['C:\\src\\____GitProjects\\CreateExecFromPy'], binaries=[], datas=[], hiddenimports=[], hookspath=[], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=None, noarchive=False,)
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='CreateExecFromPy',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,)

View File

@ -1,115 +0,0 @@
# -*- 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")

View File

@ -1,55 +0,0 @@
# -*- coding: utf-8 -*-
"""
Configuration constants for the PyInstaller GUI Wrapper application.
"""
# Default values for PyInstaller options managed by the GUI
# These are used initially and when resetting options.
DEFAULT_SPEC_OPTIONS = {
'onefile': False, # Corresponds to --onefile/--onedir
'windowed': True, # Corresponds to --windowed/--console (--noconsole)
'name': '', # Application name, derived from script if empty
'icon': '', # Path to the application icon (.ico on Win, .icns on Mac)
'add_data': [], # List of tuples (source_path, destination_in_bundle)
'hidden_imports': [], # List of modules PyInstaller might miss
'log_level': 'INFO', # PyInstaller log verbosity
'clean_build': True, # Corresponds to --clean (remove cache before build)
'confirm_overwrite': False,# Corresponds to --noconfirm (True means --noconfirm is active)
'output_dir_name': '_dist', # Custom name for the output directory (default is 'dist')
'work_dir_name': '_build', # Custom name for the temporary build directory (default is 'build')
'use_upx': True, # Whether to attempt using UPX if available
# Internal tracking, potentially populated by parser, used by generator
'scripts': [], # Track main script(s) from Analysis
'pathex': [], # Track pathex from Analysis
'binaries': [], # Track binaries from Analysis
}
# Log levels available in PyInstaller (for potential dropdown/validation)
PYINSTALLER_LOG_LEVELS = ["DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"]
# Add other constants here as needed, e.g., file type filters
SCRIPT_FILE_TYPES = [
("Python files", "*.py"),
("Pythonw files", "*.pyw"),
("All files", "*.*")
]
ICON_FILE_TYPES_WINDOWS = [
("Icon files", "*.ico"),
("All files", "*.*")
]
ICON_DEFAULT_EXT_WINDOWS = ".ico"
ICON_FILE_TYPES_MACOS = [
("Apple Icon files", "*.icns"),
("PNG images", "*.png"),
("All files", "*.*")
]
ICON_DEFAULT_EXT_MACOS = ".icns"
ICON_FILE_TYPES_LINUX = [
("Image files", "*.png"),
("Icon files", "*.ico"),
("All files", "*.*")
]
ICON_DEFAULT_EXT_LINUX = ".png"

460
gui.py
View File

@ -1,460 +0,0 @@
# -*- 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 # Needed for Add Data destination prompt
import os # Needed for path manipulation and environ
import sys # Needed for sys.platform
import threading
import queue
import traceback # For detailed error logging
import shutil # Needed for shutil.which()
# Import other modules from the package/directory
import config
import spec_parser
import builder
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("PyInstaller GUI Wrapper")
# Consider setting a minimum size, especially with the added tab
# self.minsize(650, 550)
# --- Tkinter Variables (Basic Options) ---
self.python_script_path = tk.StringVar()
self.spec_file_path = tk.StringVar()
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'])
# --- Data Structures for Complex Options ---
# List to hold tuples: (absolute_source_path, destination_in_bundle)
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()
# Start monitoring the build queue
self.after(100, self._check_build_queue)
self._log_to_gui("Applicazione inizializzata.", level="INFO")
# --- Widget Creation ---
def _create_widgets(self, parent_frame):
""" Create all the necessary GUI widgets. """
# --- Section 1: Input Script ---
self.file_frame = ttk.LabelFrame(parent_frame, text="Input Script")
self.script_label = ttk.Label(self.file_frame, text="Python Script:")
self.script_entry = ttk.Entry(self.file_frame, textvariable=self.python_script_path, state="readonly", width=70)
self.script_button_browse = ttk.Button(self.file_frame, text="Sfoglia...", command=self._select_script)
# --- 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="Opzioni Base")
self.name_label = ttk.Label(self.basic_options_frame, text="Nome App:")
self.name_entry = ttk.Entry(self.basic_options_frame, textvariable=self.app_name)
self.icon_label = ttk.Label(self.basic_options_frame, text="File Icona:")
self.icon_entry = ttk.Entry(self.basic_options_frame, textvariable=self.icon_path, state="readonly", width=60)
self.icon_button_browse = ttk.Button(self.basic_options_frame, text="Sfoglia...", command=self._select_icon)
self.icon_button_clear = ttk.Button(self.basic_options_frame, text="Pulisci", command=self._clear_icon)
self.onefile_check = ttk.Checkbutton(self.basic_options_frame, text="File Singolo (Unico eseguibile)", variable=self.is_onefile)
self.windowed_check = ttk.Checkbutton(self.basic_options_frame, text="Finestra (Nessuna console)", variable=self.is_windowed)
# --- Tab 2: File Aggiuntivi ---
self.data_files_frame = ttk.Frame(self.options_notebook, padding="10")
self.options_notebook.add(self.data_files_frame, text="File Aggiuntivi")
self.data_list_label = ttk.Label(self.data_files_frame, text="File/Cartelle da includere (Sorgente -> Destinazione nel 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, # Allow only one selection for removal
yscrollcommand=self.data_list_scrollbar_y.set,
xscrollcommand=self.data_list_scrollbar_x.set,
height=6, width=70
)
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="Aggiungi File...", command=self._add_data_file)
self.data_button_add_dir = ttk.Button(self.data_buttons_frame, text="Aggiungi Cartella...", command=self._add_data_directory)
self.data_button_remove = ttk.Button(self.data_buttons_frame, text="Rimuovi Selezionato", command=self._remove_selected_data)
# --- Section 3: Build Output Log ---
self.output_frame = ttk.LabelFrame(parent_frame, text="Log di Build")
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="Crea Eseguibile", command=self._start_build, state="disabled")
# --- Layout Widgets ---
def _layout_widgets(self):
""" Arrange widgets using grid layout. """
self.main_frame.columnconfigure(0, weight=1)
self.main_frame.rowconfigure(1, weight=0) # Notebook row
self.main_frame.rowconfigure(2, weight=1) # Output log expands
# Layout Script Selection
self.file_frame.grid(row=0, column=0, padx=5, pady=(0, 5), sticky="ew")
self.file_frame.columnconfigure(1, weight=1)
self.script_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.script_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
self.script_button_browse.grid(row=0, column=2, padx=5, pady=5)
# Layout Options Notebook
self.options_notebook.grid(row=1, column=0, padx=5, pady=5, sticky="ew")
# --- Layout Basic Options Tab ---
self.basic_options_frame.columnconfigure(1, weight=1)
self.name_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.name_entry.grid(row=0, column=1, columnspan=3, padx=5, pady=5, sticky="ew")
self.icon_label.grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.icon_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
self.icon_button_browse.grid(row=1, column=2, padx=(0, 2), pady=5)
self.icon_button_clear.grid(row=1, column=3, padx=(2, 5), pady=5)
self.onefile_check.grid(row=2, column=0, columnspan=4, padx=5, pady=5, sticky="w")
self.windowed_check.grid(row=3, column=0, columnspan=4, padx=5, pady=5, sticky="w")
# --- Layout Data Files Tab ---
self.data_files_frame.columnconfigure(0, weight=1) # Listbox expands horizontally
self.data_files_frame.rowconfigure(1, weight=1) # Listbox expands vertically
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)
# Layout Build Output Log
self.output_frame.grid(row=2, 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")
# Layout Action Frame
self.action_frame.grid(row=3, column=0, padx=5, pady=(5, 0), sticky="e")
self.build_button.pack(pady=5)
# --- Callback Methods ---
def _select_script(self):
""" Handles script selection, derives defaults, checks spec, updates GUI. """
self._log_to_gui("="*20 + " SELEZIONE SCRIPT INIZIATA " + "="*20, level="INFO")
file_path = filedialog.askopenfilename(title="Seleziona Script Python", filetypes=config.SCRIPT_FILE_TYPES)
if not file_path: self._log_to_gui("Selezione script cancellata.", level="INFO"); return
self.python_script_path.set(file_path)
self._log_to_gui(f"Selezionato script: {file_path}", level="INFO")
script_dir = os.path.dirname(file_path)
script_filename = os.path.basename(file_path)
script_name_without_ext = os.path.splitext(script_filename)[0]
potential_spec_path = os.path.join(script_dir, f"{script_name_without_ext}.spec")
self.spec_file_path.set(potential_spec_path)
self._reset_options_to_defaults(derive_name=True) # Reset includes data list
self._handle_existing_spec_file(potential_spec_path) # Parse spec if exists
self.build_button.config(state="normal")
self._log_to_gui("Pronto. Rivedi le opzioni o clicca 'Crea Eseguibile'.", level="INFO")
def _handle_existing_spec_file(self, spec_path):
""" Checks for and parses an existing spec file using spec_parser. """
if os.path.exists(spec_path):
self._log_to_gui("-" * 15 + " Parsing Spec File Esistente " + "-" * 15, level="INFO")
self._log_to_gui(f"Trovato file spec esistente: {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) # Includes data list
messagebox.showinfo("Spec File Caricato", f"Caricate opzioni da:\n{spec_path}\nCampi GUI aggiornati.")
else:
messagebox.showwarning("Spec File Parsato - Opzioni non Riconosciute", f"Parsato file spec:\n{spec_path}\n\nNessuna opzione comune riconosciuta.\nBuild userà impostazioni GUI e sovrascriverà lo spec.")
else:
messagebox.showerror("Errore Parsing Spec File", f"Fallito il parsing del file spec:\n{spec_path}\n\nControlla log. Procedere sovrascriverà lo spec.")
else:
self._log_to_gui(f"Nessun file spec trovato in: {spec_path}. Ne verrà generato uno nuovo.", level="INFO")
def _reset_options_to_defaults(self, derive_name=True):
""" Resets GUI option widgets and internal data lists to defaults. """
self._log_to_gui("Resetting opzioni ai valori di default.", level="INFO")
default_app_name = ""
if derive_name:
script_file = self.python_script_path.get()
if script_file: default_app_name = os.path.splitext(os.path.basename(script_file))[0]
self.app_name.set(default_app_name or config.DEFAULT_SPEC_OPTIONS['name'])
self.icon_path.set(config.DEFAULT_SPEC_OPTIONS['icon'])
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'])
self.added_data_list.clear()
self.data_listbox.delete(0, tk.END)
self._log_to_gui("Lista file aggiuntivi resettata.", level="DEBUG")
def _update_gui_from_options(self, options):
""" Updates GUI widgets AND data lists based on parsed options. """
self._log_to_gui("Aggiornamento GUI dalle opzioni spec caricate/parsate.", level="INFO")
# Update basic options
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: self.icon_path.set(options['icon'])
else: self.icon_path.set("")
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'])
if 'log_level' in options and options.get('log_level') in config.PYINSTALLER_LOG_LEVELS: self.log_level.set(options['log_level'])
# Update Added Data list/listbox
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"Trovati 'datas' nello spec: {len(parsed_datas)} voci.", level="INFO")
script_dir = os.path.dirname(os.path.abspath(self.python_script_path.get())) if self.python_script_path.get() else ""
for item in parsed_datas:
if isinstance(item, (tuple, list)) and len(item) == 2:
source, destination = item
abs_source = source
# Convert relative source paths based on the script's directory
if script_dir and isinstance(source, str) and not os.path.isabs(source):
abs_source = os.path.abspath(os.path.join(script_dir, source))
self._log_to_gui(f" Risolto percorso relativo data: '{source}' -> '{abs_source}'", level="DEBUG")
if isinstance(abs_source, str) and isinstance(destination, str):
self.added_data_list.append((abs_source, destination))
self.data_listbox.insert(tk.END, f"{abs_source} -> {destination}")
else: self._log_to_gui(f" Ignorata voce 'datas' non valida (non stringhe): {item}", level="WARNING")
else: self._log_to_gui(f" Ignorata voce 'datas' non valida (formato non tupla/lista di 2): {item}", level="WARNING")
self._log_to_gui("Lista file aggiuntivi popolata dallo spec.", level="DEBUG")
else: self._log_to_gui("Nessuna voce 'datas' trovata o valida nello spec.", level="DEBUG")
self._log_to_gui("Aggiornamento GUI dalle opzioni completato.", level="INFO")
def _select_icon(self):
""" Handles icon file selection based on platform. """
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="Seleziona Icona Applicazione", filetypes=filetypes, defaultextension=default_ext)
if file_path: self.icon_path.set(file_path); self._log_to_gui(f"Selezionata icona: {file_path}")
def _clear_icon(self):
""" Clears the icon selection. """
self.icon_path.set("")
self._log_to_gui("Selezione icona rimossa.")
# --- Methods for Added Data ---
def _add_data_file(self):
""" Adds a selected file to the 'add_data' list. """
source_path = filedialog.askopenfilename(title="Seleziona File Sorgente da Aggiungere")
if not source_path: self._log_to_gui("Aggiunta file annullata.", level="DEBUG"); return
abs_source_path = os.path.abspath(source_path)
dest_path = simpledialog.askstring("Destinazione nel Bundle", f"Percorso destinazione relativo per:\n{os.path.basename(abs_source_path)}\n(es. '.', 'data', 'images/subdir')", initialvalue=".")
if dest_path is None: self._log_to_gui("Aggiunta file annullata (destinazione).", level="DEBUG"); return
entry = (abs_source_path, dest_path); self.added_data_list.append(entry)
display_text = f"{abs_source_path} -> {dest_path}"; self.data_listbox.insert(tk.END, display_text)
self._log_to_gui(f"Aggiunto file dati: {display_text}", level="INFO")
def _add_data_directory(self):
""" Adds a selected directory to the 'add_data' list. """
source_path = filedialog.askdirectory(title="Seleziona Cartella Sorgente da Aggiungere")
if not source_path: self._log_to_gui("Aggiunta cartella annullata.", level="DEBUG"); return
abs_source_path = os.path.abspath(source_path)
default_dest = os.path.basename(abs_source_path)
dest_path = simpledialog.askstring("Destinazione nel Bundle", f"Percorso destinazione relativo per cartella:\n{default_dest}\n(es. '{default_dest}', 'data/{default_dest}')", initialvalue=default_dest)
if dest_path is None: self._log_to_gui("Aggiunta cartella annullata (destinazione).", level="DEBUG"); return
entry = (abs_source_path, dest_path); self.added_data_list.append(entry)
display_text = f"{abs_source_path} -> {dest_path}"; self.data_listbox.insert(tk.END, display_text)
self._log_to_gui(f"Aggiunta cartella dati: {display_text}", level="INFO")
def _remove_selected_data(self):
""" Removes the selected item from the 'add_data' list. """
selected_indices = self.data_listbox.curselection()
if not selected_indices: messagebox.showwarning("Nessuna Selezione", "Seleziona una voce da rimuovere."); return
index = selected_indices[0]
item_text = self.data_listbox.get(index); self.data_listbox.delete(index)
if index < len(self.added_data_list): removed_entry = self.added_data_list.pop(index); self._log_to_gui(f"Rimossa voce dati: {item_text}", level="INFO")
else: self._log_to_gui(f"Errore: Indice {index} fuori range per lista dati interna.", level="ERROR")
# --- Logging and Queue Handling ---
def _log_to_gui(self, message, level="INFO"):
""" Appends a formatted message to the output log area safely. """
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):
""" Internal method to append text to the ScrolledText widget. """
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] Errore aggiornamento log widget: {e}\n{traceback.format_exc()}")
def _check_build_queue(self):
""" Periodically checks the build queue for messages from the builder thread. """
try:
while True:
item = self.build_queue.get_nowait()
if isinstance(item, tuple) and len(item) == 2:
command, data = item
if command == "LOG": self._update_output_log_widget(data)
elif command == "BUILD_SUCCESS": self._log_to_gui(data, level="SUCCESS"); messagebox.showinfo("Build Completata", data); self._on_build_finished()
elif command == "BUILD_ERROR": self._log_to_gui(data, level="ERROR"); messagebox.showerror("Build Fallita", data); self._on_build_finished()
elif command == "BUILD_FINISHED": self._log_to_gui("Segnale completamento processo build ricevuto.", level="DEBUG")
else: self._log_to_gui(f"Comando sconosciuto dalla coda: {command}", level="WARNING")
else: self._log_to_gui(f"Elemento inatteso nella coda: {item}", level="WARNING")
self.build_queue.task_done()
except queue.Empty: pass
except Exception as e: self._log_to_gui(f"Errore processamento coda build: {e}\n{traceback.format_exc()}", level="CRITICAL")
finally: self.after(100, self._check_build_queue)
def _on_build_finished(self):
""" Actions to take when build completes (success or failure). """
self.build_button.config(state="normal")
self.build_thread = None
self._log_to_gui("Processo di build terminato. Controlli GUI riabilitati.", level="INFO")
# --- Spec File Generation ---
def _generate_spec_content(self):
""" Generates the .spec file content including 'datas' from the GUI list. """
script_path = self.python_script_path.get()
if not script_path or not os.path.exists(script_path): messagebox.showerror("Errore", "Percorso script Python invalido."); return None
self._log_to_gui("Generazione contenuto .spec dalle opzioni GUI...", level="INFO")
script_abspath = os.path.abspath(script_path); script_dir = os.path.dirname(script_abspath)
app_name = self.app_name.get() or os.path.splitext(os.path.basename(script_abspath))[0]
icon_path = self.icon_path.get()
icon_abspath = os.path.abspath(icon_path) if icon_path and os.path.exists(icon_path) else None
def escape_path(p): return p.replace("\\", "\\\\") if p else ""
# --- Analysis Section ---
analysis_scripts = f"['{escape_path(script_abspath)}']"
analysis_pathex = f"['{escape_path(script_dir)}']"
# Use self.added_data_list to generate the 'datas' argument
formatted_datas = "[" + ",\n ".join([f"('{escape_path(src)}', '{escape_path(dst)}')" for src, dst in self.added_data_list]) + "]"
self._log_to_gui(f"Formattati {len(self.added_data_list)} voci 'datas' per lo spec.", level="DEBUG")
formatted_hiddenimports = "[]"; formatted_binaries = "[]" # Placeholders for now
analysis_str = f"""a = Analysis(scripts={analysis_scripts}, pathex={analysis_pathex}, binaries={formatted_binaries}, datas={formatted_datas}, hiddenimports={formatted_hiddenimports}, hookspath=[], 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_options = [f"name='{app_name}'", "debug=False", "bootloader_ignore_signals=False", "strip=False", f"upx={config.DEFAULT_SPEC_OPTIONS['use_upx']}", "upx_exclude=[]", "runtime_tmpdir=None", f"console={not self.is_windowed.get()}"]
if icon_abspath: exe_options.append(f"icon='{escape_path(icon_abspath)}'")
exe_options_str = ",\n ".join(exe_options)
exe_str = f"""exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], {exe_options_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}',)"""
spec_header = "# -*- mode: python ; coding: utf-8 -*-\n\nblock_cipher = None\n"
spec_content = f"{spec_header}\n{analysis_str}\n\n{pyz_str}\n\n{exe_str}\n{collect_str}"
self._log_to_gui("Contenuto file .spec generato.", level="DEBUG")
return spec_content.strip() + "\n"
# --- Save Spec File ---
def _save_spec_file(self, content):
""" Saves the generated content to the .spec file path. """
spec_path = self.spec_file_path.get()
if not spec_path: messagebox.showerror("Errore", "Impossibile salvare spec: percorso non impostato."); return False
self._log_to_gui(f"Tentativo salvataggio spec file su: {spec_path}", level="INFO")
try:
os.makedirs(os.path.dirname(spec_path), exist_ok=True)
with open(spec_path, 'w', encoding='utf-8') as f: f.write(content)
self._log_to_gui(f"File spec salvato con successo: {spec_path}", level="INFO"); return True
except Exception as e:
error_msg = f"Fallito salvataggio spec file '{spec_path}': {e}\n{traceback.format_exc()}"
self._log_to_gui(error_msg, level="ERROR"); messagebox.showerror("Errore Salvataggio File", error_msg); return False
# --- Build Execution ---
def _start_build(self):
""" Validates, generates/saves spec, starts build thread using the 'pyinstaller' command
with a potentially cleaned environment for Tcl/Tk variables. """
self._log_to_gui("="*20 + " PROCESSO DI BUILD INIZIATO " + "="*20, level="INFO")
script_path = self.python_script_path.get(); spec_path = self.spec_file_path.get()
if not script_path or not os.path.exists(script_path): messagebox.showerror("Errore", "Seleziona uno script Python valido."); self._log_to_gui("Build annullata: script non valido.", level="ERROR"); return
if not spec_path: messagebox.showerror("Errore", "Errore interno: percorso spec non impostato."); self._log_to_gui("Build annullata: percorso spec non impostato.", level="ERROR"); return
self._log_to_gui("Generazione contenuto spec dalle opzioni GUI...", level="INFO")
spec_content = self._generate_spec_content();
if spec_content is None: self._log_to_gui("Build annullata: fallita generazione spec.", level="ERROR"); return
self._log_to_gui("Salvataggio file spec...", level="INFO")
if not self._save_spec_file(spec_content): self._log_to_gui("Build annullata: fallito salvataggio spec.", level="ERROR"); return
self._log_to_gui("Preparazione esecuzione PyInstaller...", level="INFO")
self.build_button.config(state="disabled")
script_dir = os.path.dirname(os.path.abspath(script_path))
dist_path_abs = os.path.join(script_dir, config.DEFAULT_SPEC_OPTIONS['output_dir_name'])
work_path_abs = os.path.join(script_dir, config.DEFAULT_SPEC_OPTIONS['work_dir_name'])
pyinstaller_cmd = shutil.which("pyinstaller")
if not pyinstaller_cmd:
error_msg = ("Comando 'pyinstaller' non trovato nel PATH...\n"
"Assicurati sia installato e nel PATH.")
self._log_to_gui(error_msg, level="CRITICAL"); messagebox.showerror("Errore PyInstaller", error_msg)
self.build_button.config(state="normal"); return
self._log_to_gui(f"Trovato eseguibile PyInstaller in: {pyinstaller_cmd}", level="DEBUG")
command = [pyinstaller_cmd, spec_path, "--distpath", dist_path_abs, "--workpath", work_path_abs, "--log-level", self.log_level.get()]
if config.DEFAULT_SPEC_OPTIONS['clean_build']: command.append("--clean")
if not config.DEFAULT_SPEC_OPTIONS['confirm_overwrite']: command.append("--noconfirm")
build_env = os.environ.copy()
removed_vars = []
if 'TCL_LIBRARY' in build_env: del build_env['TCL_LIBRARY']; removed_vars.append('TCL_LIBRARY')
if 'TK_LIBRARY' in build_env: del build_env['TK_LIBRARY']; removed_vars.append('TK_LIBRARY')
if removed_vars: self._log_to_gui(f"Rimosse variabili d'ambiente {removed_vars} per PyInstaller esterno.", level="DEBUG")
else: self._log_to_gui("Nessuna variabile Tcl/Tk ereditata trovata da rimuovere.", level="DEBUG")
quoted_command = [f'"{arg}"' if ' ' in arg else arg for arg in command]
self._log_to_gui(f"Comando base: {' '.join(quoted_command)}", level="DEBUG")
self._log_to_gui(f"Directory di lavoro: {script_dir}", level="DEBUG")
self.build_thread = threading.Thread(
target=builder.run_build_in_thread,
args=(command, script_dir, self.build_queue, self._log_to_gui, config.DEFAULT_SPEC_OPTIONS['output_dir_name'], build_env), # Passa ambiente
daemon=True
)
self.build_thread.start()
self._log_to_gui("Thread di build avviato con ambiente personalizzato.", level="INFO")
# --- End of PyInstallerGUI class ---
# Note: The main execution block remains in CreateExecFromPy.py
# if __name__ == "__main__":
# app = PyInstallerGUI()
# app.mainloop()

View File

@ -0,0 +1,63 @@
# -*- mode: python ; coding: utf-8 -*-
# This spec file is for building the PyInstaller GUI Wrapper tool itself.
block_cipher = None
a = Analysis(
# MODIFIED: Point to the new entry point within the subpackage
scripts=['pyinstallerguiwrapper/__main__.py'],
# MODIFIED: Include the root directory '.' so PyInstaller finds the subpackage 'pyinstallerguiwrapper'
pathex=['.'],
binaries=[],
datas=[], # No data files needed for the tool itself currently
hiddenimports=[], # Add any modules PyInstaller might miss (e.g., sometimes needed for pkg_resources)
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
# MODIFIED: Set the application name
name='PyInstallerGUIWrapper',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True, # Use UPX if available (optional)
runtime_tmpdir=None,
# MODIFIED: Set console=False for a GUI application
console=False,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
# MODIFIED: Add icon if pyinstallerguiwrapper.ico exists in the root
icon='pyinstallerguiwrapper.ico' # Assumes the icon file is in the same directory as the spec file
)
# Use COLLECT for one-dir builds (recommended for GUI stability)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True, # Match UPX setting
upx_exclude=[],
# MODIFIED: Set the output directory name
name='PyInstallerGUIWrapper' # Name of the folder in _dist/
)
# If you wanted a one-file build instead (can sometimes be less stable for Tkinter):
# Remove the 'coll = COLLECT(...)' block above
# And potentially adjust EXE options (though Analysis usually handles datas/binaries correctly now)

View File

@ -8,12 +8,12 @@ Run this file to launch the application.
import sys
import os
import traceback # Import traceback for better error reporting
import traceback # Import traceback for better error reporting
# --- Crucial: Add the directory containing the modules to sys.path ---
# This allows Python to find 'gui.py', 'config.py', etc. when run directly.
script_dir = os.path.dirname(os.path.abspath(__file__))
print(f"Adding to sys.path: {script_dir}") # Debug print
print(f"Adding to sys.path: {script_dir}") # Debug print
if script_dir not in sys.path:
sys.path.insert(0, script_dir)
@ -22,6 +22,7 @@ if script_dir not in sys.path:
try:
# Import the main GUI class directly by name
from gui import PyInstallerGUI
# We might need other modules here later if needed at top level
# import config
# import builder
@ -30,7 +31,7 @@ except ImportError as e:
print(f"ERROR: Could not import required modules.")
print(f"ImportError: {e}")
print(f"Current sys.path: {sys.path}")
traceback.print_exc() # Print detailed traceback
traceback.print_exc() # Print detailed traceback
sys.exit(1)
except Exception as e:
print(f"ERROR: An unexpected error occurred during initial imports: {e}")
@ -46,22 +47,29 @@ def main():
app = PyInstallerGUI()
app.mainloop()
except Exception as e:
print(f"FATAL ERROR: An unexpected error occurred during application execution: {e}")
print(
f"FATAL ERROR: An unexpected error occurred during application execution: {e}"
)
traceback.print_exc()
try:
# Attempt to show a Tkinter error box if possible
import tkinter as tk
from tkinter import messagebox
root = tk.Tk()
root.withdraw()
messagebox.showerror("Fatal Error", f"Application failed to run:\n{e}\n\nSee console for details.")
messagebox.showerror(
"Fatal Error",
f"Application failed to run:\n{e}\n\nSee console for details.",
)
root.destroy()
except Exception:
pass # Ignore if Tkinter fails here too
pass # Ignore if Tkinter fails here too
sys.exit(1)
# --- Main Execution Guard ---
if __name__ == "__main__":
# Check Python version if needed (Tkinter themes benefit from newer versions)
# print(f"Running with Python {sys.version}")
main()
main()

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
# Intentionally left blank to indicate this directory is a Python package.

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
"""
Main entry point script for the PyInstaller GUI Wrapper application when run as a module.
"""
import sys
import os
import traceback # Import traceback for better error reporting
# --- No sys.path manipulation needed when running as a module ---
# Python handles the package structure automatically.
# --- Import the necessary modules ---
try:
# MODIFIED: Use relative import to get the GUI class from within the same package
from .gui import PyInstallerGUI
except ImportError as e:
# Provide more context in case of import errors
package_dir = os.path.dirname(os.path.abspath(__file__))
print(f"ERROR: Could not import required modules from package.")
print(f" Attempted relative import within: {package_dir}")
print(f"ImportError: {e}")
print(f"Current sys.path: {sys.path}")
traceback.print_exc() # Print detailed traceback
sys.exit(1)
except Exception as e:
print(f"ERROR: An unexpected error occurred during initial imports: {e}")
traceback.print_exc()
sys.exit(1)
def main():
"""
Initializes and runs the PyInstaller GUI application.
"""
try:
app = PyInstallerGUI()
app.mainloop()
except Exception as e:
# MODIFIED: Error message context updated
print(
f"FATAL ERROR: An unexpected error occurred during application execution: {e}"
)
traceback.print_exc()
try:
# Attempt to show a Tkinter error box if possible
import tkinter as tk
from tkinter import messagebox
root = tk.Tk()
root.withdraw()
# MODIFIED: Messagebox content translated/updated
messagebox.showerror(
"Fatal Error",
f"Application failed to run:\n{e}\n\nSee console for details.",
)
root.destroy()
except Exception:
pass # Ignore if Tkinter fails here too
sys.exit(1)
# --- Main Execution Guard ---
# This guard is still useful if you were to potentially import `__main__` elsewhere,
# though it's less critical when the primary execution path is `python -m`.
if __name__ == "__main__":
# print(f"Running PyInstallerGUIWrapper via __main__.py with Python {sys.version}")
main()

View File

@ -0,0 +1,67 @@
# -*- 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
# NO CHANGES NEEDED in this file for the restructuring
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.
... (docstring remains the same) ...
"""
# ... (Function body remains the same as the previously translated version) ...
build_process = None
try:
logger_func("Build thread starting execution...", level="INFO")
env_log_msg = f"Using {'custom' if environment else 'inherited'} environment."
if environment: pass
logger_func(f"PyInstaller process starting. {env_log_msg}", level="INFO")
build_process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=working_dir,
text=True, encoding='utf-8', errors='replace', bufsize=1, env=environment )
logger_func(f"PyInstaller process launched (PID: {build_process.pid}).", level="INFO")
while True:
try:
line = build_process.stdout.readline()
if not line:
if build_process.poll() is not None:
logger_func("End of PyInstaller output stream.", level="DEBUG")
break
else: pass
else: output_queue.put(("LOG", line))
except Exception as read_err:
logger_func(f"Error reading PyInstaller output stream: {read_err}\n{traceback.format_exc()}", level="ERROR")
break
return_code = build_process.wait()
logger_func(f"PyInstaller process finished with exit code: {return_code}", level="INFO")
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 completed successfully!\nOutput Directory: {dist_path_abs}"
output_queue.put(("BUILD_SUCCESS", success_msg))
else:
warn_msg = (f"Build finished with exit code 0, but output directory not found:\n"
f"{dist_path_abs}\nCheck the full log for potential issues.")
logger_func(warn_msg, level="WARNING")
output_queue.put(("BUILD_ERROR", warn_msg))
else:
error_msg = f"Build failed! (Exit Code: {return_code})\nCheck the log for details."
output_queue.put(("BUILD_ERROR", error_msg))
except FileNotFoundError:
error_msg = ("Error: 'pyinstaller' command not found.\n"
"Ensure PyInstaller is installed correctly and in the system PATH.")
output_queue.put(("BUILD_ERROR", error_msg))
logger_func(error_msg, level="CRITICAL")
except Exception as e:
error_msg = f"Unexpected error during build process execution: {e}"
output_queue.put(("BUILD_ERROR", error_msg))
logger_func(error_msg, level="CRITICAL")
logger_func(traceback.format_exc(), level="DEBUG")
finally:
output_queue.put(("BUILD_FINISHED", None))
logger_func("Build thread finished.", level="INFO")

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
"""
Configuration constants for the PyInstaller GUI Wrapper application.
"""
# NO CHANGES NEEDED in this file for the restructuring
# Default values for PyInstaller options managed by the GUI
DEFAULT_SPEC_OPTIONS = {
'onefile': False,
'windowed': True, # Corresponds to --windowed/--console (--noconsole)
'name': '',
'icon': '',
'add_data': [],
'hidden_imports': [],
'log_level': 'INFO',
'clean_build': True, # Corresponds to --clean
'confirm_overwrite': False,# Corresponds to --noconfirm (True means --noconfirm is active)
'output_dir_name': '_dist', # Default name for the output directory
'work_dir_name': '_build', # Default name for the temporary build directory
'use_upx': True,
# Internal tracking - Populated by parser, used by GUI/generator
'scripts': [],
'pathex': [],
'binaries': [],
'datas': [], # Changed from 'add_data' to match PyInstaller terminology
}
# Log levels available in PyInstaller
PYINSTALLER_LOG_LEVELS = ["DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"]
# File type filters (Descriptions are already English)
SCRIPT_FILE_TYPES = [
("Python files", "*.py"),
("Pythonw files", "*.pyw"),
("All files", "*.*")
]
ICON_FILE_TYPES_WINDOWS = [
("Icon files", "*.ico"),
("All files", "*.*")
]
ICON_DEFAULT_EXT_WINDOWS = ".ico"
ICON_FILE_TYPES_MACOS = [
("Apple Icon files", "*.icns"),
("PNG images", "*.png"),
("All files", "*.*")
]
ICON_DEFAULT_EXT_MACOS = ".icns"
ICON_FILE_TYPES_LINUX = [
("Image files", "*.png"),
("Icon files", "*.ico"),
("All files", "*.*")
]
ICON_DEFAULT_EXT_LINUX = ".png"

1031
pyinstallerguiwrapper/gui.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,210 +7,152 @@ Provides a safe way to extract configuration without executing the spec file.
import ast
import os
# NO CHANGES NEEDED in this file for the restructuring
class _SpecVisitor(ast.NodeVisitor):
"""
An AST Node Visitor to traverse the Python code of a .spec file
and extract relevant PyInstaller configuration details safely.
Reduces logging noise by being more selective about evaluation attempts.
"""
def __init__(self, logger_func=print):
"""
Initialize the visitor.
Args:
logger_func (callable): Function for logging (takes message, level).
"""
self.parsed_options = {}
self.logger = logger_func
self._variable_assignments = {}
def _log(self, message, level="DEBUG"):
""" Helper to call the logger function. """
try:
self.logger(message, level=level)
except TypeError:
self.logger(f"[{level}] {message}")
def _evaluate_node(self, node):
"""
Attempts to evaluate the value of an AST node more quietly.
Handles constants, simple lists/tuples/dicts, and tracked variables.
Returns evaluated Python value, or None if evaluation is complex/unsafe.
Logs warnings ONLY for unresolved variables or errors within structures.
"""
# --- Handle Simple Constants ---
if isinstance(node, ast.Constant): # Python 3.8+
return node.value
elif isinstance(node, (ast.Str, ast.Num, ast.NameConstant)): # Python < 3.8 compat
# Handle deprecated nodes
# ... (Function body remains the same as previous version) ...
if isinstance(node, ast.Constant): return node.value
elif isinstance(node, (ast.Str, ast.Num, ast.NameConstant)):
if isinstance(node, ast.Str): return node.s
if isinstance(node, ast.Num): return node.n
return node.value # For True, False, None
# --- Handle Simple Lists/Tuples ---
return node.value
elif isinstance(node, (ast.List, ast.Tuple)):
try:
elements = [self._evaluate_node(el) for el in node.elts]
# Check if ANY element failed evaluation (returned None)
if any(el is None for el in elements):
# Log warning here, as we expected to evaluate the elements
self._log(f"Could not evaluate all elements in list/tuple: {ast.dump(node)}", level="WARNING")
return None # Cannot reliably evaluate the whole structure
# If all elements evaluated, return the list/tuple
return None
return elements
except Exception as e:
self._log(f"Error evaluating list/tuple contents: {e} in {ast.dump(node)}", level="WARNING")
return None
# --- Handle Simple Dictionaries ---
elif isinstance(node, ast.Dict):
try:
keys = [self._evaluate_node(k) for k in node.keys]
values = [self._evaluate_node(v) for v in node.values]
# Check if ANY key or value failed evaluation
if any(k is None for k in keys) or any(v is None for v in values):
self._log(f"Could not evaluate all keys/values in dict: {ast.dump(node)}", level="WARNING")
return None
# If all evaluated, return the dict
return dict(zip(keys, values))
except Exception as e:
self._log(f"Error evaluating dict contents: {e} in {ast.dump(node)}", level="WARNING")
return None
# --- Handle Variable Names ---
elif isinstance(node, ast.Name):
if node.id in self._variable_assignments:
return self._variable_assignments[node.id]
else:
# Log warning here, as we failed to resolve a name we encountered
self._log(f"Cannot resolve variable name during evaluation: {node.id}", level="WARNING")
return None
# --- Handle Other Node Types Silently ---
else:
# For types like ast.Call, ast.Attribute, ast.BinOp, etc.,
# we assume they are too complex to evaluate safely or are not
# simple constants we care about. Return None without logging a warning.
# self._log(f"Skipping evaluation for node type: {type(node)}", level="DEBUG") # Optional debug log
return None
return None # Skip complex nodes quietly
def visit_Assign(self, node):
"""
Visits assignment statements. Tries to track assignments only if the
value appears to be a simple constant or structure. Avoids warnings
for assignments like 'a = Analysis(...)'.
"""
# Check if assignment is to a single variable name
# ... (Function body remains the same as previous version) ...
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
var_name = node.targets[0].id
# --- Check if the value is likely evaluatable BEFORE calling _evaluate_node ---
# Only proceed if it looks like a constant, simple structure, or known variable name
if isinstance(node.value, (ast.Constant, ast.Str, ast.Num, ast.NameConstant,
ast.List, ast.Tuple, ast.Dict, ast.Name)):
value = self._evaluate_node(node.value)
if value is not None:
# Successfully evaluated, track the assignment
self._variable_assignments[var_name] = value
self._log(f"Tracked assignment: {var_name} = {repr(value)}")
else:
# Evaluation attempted but failed (e.g., unresolved name inside list)
# Log a warning because we expected to evaluate this type
self._log(f"Could not evaluate value for assignment: {var_name} = {ast.dump(node.value)}", level="WARNING")
# else:
# If node.value is e.g. an ast.Call like Analysis(), PYZ(), EXE(),
# do nothing - don't try to evaluate, don't log warning.
# self._log(f"Skipping tracking for complex assignment: {var_name}", level="DEBUG") # Optional debug log
# Continue traversal regardless of whether we tracked the assignment
self.generic_visit(node)
def visit_Call(self, node):
""" Visits function call nodes (e.g., Analysis(...), EXE(...)). """
# ... (Function body remains the same as previous version) ...
if isinstance(node.func, ast.Name):
call_name = node.func.id
options = {} # Extracted args for this call
# --- Parse keyword arguments ---
options = {}
for keyword in node.keywords:
arg_name = keyword.arg
value = self._evaluate_node(keyword.value)
if value is not None:
options[arg_name] = value
else:
# Log evaluation failures for keyword args as WARNING,
# as these are more likely intended to be simple values.
self._log(f"Could not evaluate keyword argument '{arg_name}' for call '{call_name}'. Value: {ast.dump(keyword.value)}", level="WARNING")
if value is not None: options[arg_name] = value
else: self._log(f"Could not evaluate keyword argument '{arg_name}' for call '{call_name}'. Value: {ast.dump(keyword.value)}", level="WARNING")
# --- Parse positional arguments ---
# We evaluate them mainly to potentially populate fields like 'scripts'
# if they aren't provided as keywords. Failure to evaluate these
# (e.g., 'pyz' in EXE(pyz, ...)) is less critical, maybe log as DEBUG.
positional_args = []
for i, arg_node in enumerate(node.args):
arg_value = self._evaluate_node(arg_node)
positional_args.append(arg_value)
# Optional: Log failure for positional args at DEBUG level
# if arg_value is None:
# self._log(f"Could not evaluate positional argument {i} for call '{call_name}'. Value: {ast.dump(arg_node)}", level="DEBUG")
# --- Process based on function name ---
# (Logic for Analysis, EXE, COLLECT remains the same)
if call_name == 'Analysis':
self._log(f"Found Analysis() call.")
# Prioritize keyword args, fallback to positional
self.parsed_options['scripts'] = options.get('scripts')
if self.parsed_options['scripts'] is None and len(positional_args) > 0:
self.parsed_options['scripts'] = positional_args[0]
self.parsed_options['pathex'] = options.get('pathex', []) # Default to empty list
self.parsed_options['pathex'] = options.get('pathex', [])
if not self.parsed_options['pathex'] and len(positional_args) > 1:
self.parsed_options['pathex'] = positional_args[1] or [] # Use empty list if None
self.parsed_options['pathex'] = positional_args[1] or []
self.parsed_options['datas'] = options.get('datas', [])
self.parsed_options['hiddenimports'] = options.get('hiddenimports', [])
self.parsed_options['binaries'] = options.get('binaries', [])
elif call_name == 'EXE':
self._log(f"Found EXE() call.")
self.parsed_options['name'] = options.get('name')
self.parsed_options['icon'] = options.get('icon')
console_val = options.get('console')
if console_val is True: self.parsed_options['windowed'] = False
elif console_val is False: self.parsed_options['windowed'] = True
# If 'console' is None (not present or unevaluatable), 'windowed' remains unset here
# Assume onefile=True initially
self.parsed_options['onefile'] = True
self._log(f"Found EXE() call.")
# Use .get() with default None for safety
self.parsed_options['name'] = options.get('name')
# Handle icon path carefully (might be None or empty string)
icon_val = options.get('icon')
self.parsed_options['icon'] = icon_val if icon_val else None # Store None if empty or not present
console_val = options.get('console')
if console_val is True: self.parsed_options['windowed'] = False
elif console_val is False: self.parsed_options['windowed'] = True
# Assume onefile=True unless COLLECT is found
if 'onefile' not in self.parsed_options: # Avoid overwriting if set by COLLECT already
self.parsed_options['onefile'] = True # Default assumption for EXE only
elif call_name == 'COLLECT':
self._log(f"Found COLLECT() call.")
# COLLECT overrides the onefile assumption
self.parsed_options['onefile'] = False
# Continue traversal
self.parsed_options['onefile'] = False # COLLECT means it's not onefile
self.generic_visit(node)
def get_options(self):
""" Returns the dictionary of parsed options. Normalize paths if needed. """
# Normalize icon path if found
# ... (Function body remains the same as previous version) ...
if 'icon' in self.parsed_options and self.parsed_options['icon']:
try:
self.parsed_options['icon'] = os.path.normpath(self.parsed_options['icon'])
# Ensure it's a string before normpath
if isinstance(self.parsed_options['icon'], str):
self.parsed_options['icon'] = os.path.normpath(self.parsed_options['icon'])
else: # If icon is somehow not a string, log and remove
self._log(f"Invalid type for icon path: {type(self.parsed_options['icon'])}. Removing.", level="WARNING")
self.parsed_options['icon'] = None
except Exception as e:
self._log(f"Could not normalize icon path: {self.parsed_options['icon']} ({e})", level="WARNING")
self.parsed_options['icon'] = None # Set to None on error
# Ensure 'datas' is always a list, even if missing or None from spec
self.parsed_options['datas'] = self.parsed_options.get('datas', [])
if not isinstance(self.parsed_options['datas'], list):
self._log(f"Invalid type for 'datas' in spec ({type(self.parsed_options['datas'])}), resetting to empty list.", level="WARNING")
self.parsed_options['datas'] = []
return self.parsed_options
# --- The `parse_spec_file` function remains the same ---
def parse_spec_file(spec_path, logger_func=print):
""" Safely parses a .spec file using AST. """
# (No changes needed in this function itself)
# ... (Function body remains the same as previous version) ...
logger_func(f"Attempting to parse spec file using AST: {spec_path}", level="INFO")
try:
with open(spec_path, 'r', encoding='utf-8') as f: content = f.read()
tree = ast.parse(content, filename=spec_path)
visitor = _SpecVisitor(logger_func=logger_func) # Use updated Visitor
visitor = _SpecVisitor(logger_func=logger_func)
visitor.visit(tree)
parsed_options = visitor.get_options()
if not parsed_options:

View File

@ -1,8 +1,8 @@
{
"display_name": "PyInstaller GUI",
"description": "Crea eseguibili da script Python usando PyInstaller con una GUI.",
"command": ["python", "CreateExecFromPy.py"],
"version": "1.0",
"display_name": "PyInstaller GUI Wrapper",
"description": "Creates executables from Python projects using PyInstaller with a GUI.",
"command": ["python", "-m", "pyinstallerguiwrapper"],
"version": "1.1",
"parameters": [],
"has_gui": true
}