aggiunta la funzione

This commit is contained in:
VALLONGOL 2025-11-14 11:08:29 +01:00
parent 271a352540
commit d4749d35ed
2 changed files with 445 additions and 18 deletions

View File

@ -19,6 +19,7 @@ from .project_selector import ProjectManager
from .options_manager import OptionsManager from .options_manager import OptionsManager
from .output_logger import OutputLogger from .output_logger import OutputLogger
from .data_files_manager import DataFilesManager from .data_files_manager import DataFilesManager
from .simple_script_builder import SimpleScriptBuilder
from pyinstallerguiwrapper.build.version_manager import VersionManager from pyinstallerguiwrapper.build.version_manager import VersionManager
from pyinstallerguiwrapper.build.build_orchestrator import BuildOrchestrator from pyinstallerguiwrapper.build.build_orchestrator import BuildOrchestrator
from pyinstallerguiwrapper import config from pyinstallerguiwrapper import config
@ -57,6 +58,17 @@ class PyInstallerGUI(tk.Tk):
self.rowconfigure(0, weight=1) self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
# Create main mode notebook (tabs for different build modes)
self.mode_notebook = ttk.Notebook(self.main_frame)
# Create frames for each mode
self.project_mode_frame = ttk.Frame(self.mode_notebook, padding="10")
self.simple_mode_frame = ttk.Frame(self.mode_notebook, padding="10")
# Add tabs
self.mode_notebook.add(self.project_mode_frame, text="Project Mode")
self.mode_notebook.add(self.simple_mode_frame, text="Simple Script Mode")
self._create_widgets_frames_and_output_log_area() self._create_widgets_frames_and_output_log_area()
self.output_logger = OutputLogger( self.output_logger = OutputLogger(
@ -125,6 +137,11 @@ class PyInstallerGUI(tk.Tk):
update_derived_spec_label_callback=self._update_derived_spec_label_gui update_derived_spec_label_callback=self._update_derived_spec_label_gui
) )
self.simple_script_builder = SimpleScriptBuilder(
parent_frame=self.simple_mode_frame,
logger_func=self.output_logger.log_message
)
self._layout_widgets() self._layout_widgets()
self.after(100, self._check_main_gui_action_queue) self.after(100, self._check_main_gui_action_queue)
@ -184,13 +201,13 @@ class PyInstallerGUI(tk.Tk):
self.after(100, self._check_main_gui_action_queue) self.after(100, self._check_main_gui_action_queue)
def _create_widgets_frames_and_output_log_area(self) -> None: def _create_widgets_frames_and_output_log_area(self) -> None:
# Questo metodo non ha bisogno di modifiche, lo ometto per brevità # === PROJECT MODE WIDGETS (in project_mode_frame) ===
self.project_frame = ttk.LabelFrame(self.main_frame, text="Project Directory") self.project_frame = ttk.LabelFrame(self.project_mode_frame, text="Project Directory")
self.project_dir_label = ttk.Label(self.project_frame, text="Main 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_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...") self.project_dir_button_browse = ttk.Button(self.project_frame, text="Browse...")
self.derived_paths_frame = ttk.LabelFrame(self.main_frame, text="Derived Paths (Automatic)") self.derived_paths_frame = ttk.LabelFrame(self.project_mode_frame, text="Derived Paths (Automatic)")
self.derived_script_label_info = ttk.Label(self.derived_paths_frame, text="Script Found:") 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_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_info = ttk.Label(self.derived_paths_frame, text="Spec File Found:")
@ -198,7 +215,7 @@ class PyInstallerGUI(tk.Tk):
self.derived_icon_label_info = ttk.Label(self.derived_paths_frame, text="Icon Found:") 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") self.derived_icon_label_val = ttk.Label(self.derived_paths_frame, text="N/A", foreground="grey", anchor="w")
self.options_notebook = ttk.Notebook(self.main_frame) self.options_notebook = ttk.Notebook(self.project_mode_frame)
self.basic_options_frame = ttk.Frame(self.options_notebook, padding="10") 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.options_notebook.add(self.basic_options_frame, text="Basic Options (from Spec/GUI)")
@ -219,23 +236,29 @@ class PyInstallerGUI(tk.Tk):
self.data_button_add_dir = ttk.Button(self.data_buttons_frame, text="Add Folder...") self.data_button_add_dir = ttk.Button(self.data_buttons_frame, text="Add Folder...")
self.data_button_remove = ttk.Button(self.data_buttons_frame, text="Remove Selected") self.data_button_remove = ttk.Button(self.data_buttons_frame, text="Remove Selected")
self.project_action_frame = ttk.Frame(self.project_mode_frame)
self.build_button = ttk.Button(self.project_action_frame, text="Build Executable", command=self._trigger_build_process, state="disabled")
# === SHARED OUTPUT LOG (in main_frame, outside tabs) ===
self.output_frame = ttk.LabelFrame(self.main_frame, text="Build Log") self.output_frame = ttk.LabelFrame(self.main_frame, text="Build Log")
self.output_text_widget = tkinter.scrolledtext.ScrolledText(self.output_frame, wrap=tk.WORD, height=15, state="disabled") self.output_text_widget = tkinter.scrolledtext.ScrolledText(self.output_frame, wrap=tk.WORD, height=15, state="disabled")
self.action_frame = ttk.Frame(self.main_frame)
self.build_button = ttk.Button(self.action_frame, text="Build Executable", command=self._trigger_build_process, state="disabled")
def _layout_widgets(self) -> None: def _layout_widgets(self) -> None:
# Questo metodo non ha bisogno di modifiche, lo ometto per brevità # === LAYOUT PROJECT MODE TAB ===
self.project_dir_button_browse.config(command=self.project_manager.select_project_directory) self.project_dir_button_browse.config(command=self.project_manager.select_project_directory)
self.main_frame.rowconfigure(0, weight=0) self.project_mode_frame.rowconfigure(0, weight=0)
self.project_mode_frame.columnconfigure(0, weight=1)
# Project frame
self.project_frame.grid(row=0, column=0, padx=5, pady=(0, 5), sticky="ew") self.project_frame.grid(row=0, column=0, padx=5, pady=(0, 5), sticky="ew")
self.project_frame.columnconfigure(1, weight=1) 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_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_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) self.project_dir_button_browse.grid(row=0, column=2, padx=5, pady=5)
# Derived paths frame
row_idx = 1 row_idx = 1
self.main_frame.rowconfigure(row_idx, weight=0) self.project_mode_frame.rowconfigure(row_idx, weight=0)
self.derived_paths_frame.grid(row=row_idx, column=0, padx=5, pady=5, sticky="ew") 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_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_info.grid(row=0, column=0, padx=5, pady=2, sticky="w")
@ -244,9 +267,11 @@ class PyInstallerGUI(tk.Tk):
self.derived_spec_label_val.grid(row=1, column=1, padx=5, pady=2, sticky="ew") 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_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") self.derived_icon_label_val.grid(row=2, column=1, padx=5, pady=2, sticky="ew")
# Options notebook
row_idx += 1 row_idx += 1
self.main_frame.rowconfigure(row_idx, weight=0) self.project_mode_frame.rowconfigure(row_idx, weight=1)
self.options_notebook.grid(row=row_idx, column=0, padx=5, pady=5, sticky="ew") self.options_notebook.grid(row=row_idx, column=0, padx=5, pady=5, sticky="nsew")
self.data_files_frame.columnconfigure(0, weight=1) self.data_files_frame.columnconfigure(0, weight=1)
self.data_files_frame.rowconfigure(1, 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_list_label.grid(row=0, column=0, columnspan=2, padx=5, pady=(0,5), sticky="w")
@ -260,16 +285,26 @@ class PyInstallerGUI(tk.Tk):
self.data_button_add_file.config(command=self.data_files_manager.add_file) self.data_button_add_file.config(command=self.data_files_manager.add_file)
self.data_button_add_dir.config(command=self.data_files_manager.add_directory) self.data_button_add_dir.config(command=self.data_files_manager.add_directory)
self.data_button_remove.config(command=self.data_files_manager.remove_selected) self.data_button_remove.config(command=self.data_files_manager.remove_selected)
# Action frame
row_idx += 1 row_idx += 1
self.main_frame.rowconfigure(row_idx, weight=1) self.project_mode_frame.rowconfigure(row_idx, weight=0)
self.output_frame.grid(row=row_idx, column=0, padx=5, pady=5, sticky="nsew") self.project_action_frame.grid(row=row_idx, column=0, padx=5, pady=(5,0), sticky="e")
self.build_button.pack(pady=5)
# === LAYOUT MAIN FRAME (MODE NOTEBOOK + OUTPUT LOG) ===
self.main_frame.rowconfigure(0, weight=1)
self.main_frame.rowconfigure(1, weight=1)
self.main_frame.columnconfigure(0, weight=1)
# Mode notebook (tabs)
self.mode_notebook.grid(row=0, column=0, padx=5, pady=5, sticky="nsew")
# Shared output log
self.output_frame.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
self.output_frame.rowconfigure(0, weight=1) self.output_frame.rowconfigure(0, weight=1)
self.output_frame.columnconfigure(0, weight=1) self.output_frame.columnconfigure(0, weight=1)
self.output_text_widget.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") self.output_text_widget.grid(row=0, column=0, padx=5, pady=5, sticky="nsew")
row_idx += 1
self.main_frame.rowconfigure(row_idx, weight=0)
self.action_frame.grid(row=row_idx, column=0, padx=5, pady=(5,0), sticky="e")
self.build_button.pack(pady=5)
def _on_project_selected(self, project_root_str: str, derived_paths_info: Dict[str, Any]) -> None: def _on_project_selected(self, project_root_str: str, derived_paths_info: Dict[str, Any]) -> None:
# Questo metodo non ha bisogno di modifiche, lo ometto per brevità # Questo metodo non ha bisogno di modifiche, lo ometto per brevità

View File

@ -0,0 +1,392 @@
# -*- coding: utf-8 -*-
"""
Simple Script Builder for PyInstaller GUI Wrapper.
Handles building executables from single Python scripts.
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pathlib
import subprocess
import sys
import os
import threading
from typing import Optional, Callable
class SimpleScriptBuilder:
"""
Manages the simple script building interface and process.
Allows users to select a single Python script and build it into an executable.
"""
def __init__(
self,
parent_frame: ttk.Frame,
logger_func: Optional[Callable[..., None]] = None,
):
"""
Initializes the SimpleScriptBuilder.
Args:
parent_frame: The parent Tkinter frame where this interface will be placed.
logger_func: A function to use for logging messages.
"""
self.parent_frame = parent_frame
self.logger = logger_func if logger_func else self._default_logger
# Variables
self.script_path_var = tk.StringVar()
self.output_dir_var = tk.StringVar()
self.onefile_var = tk.BooleanVar(value=True)
self.is_building = False
self._create_widgets()
self._layout_widgets()
def _default_logger(self, message: str, level: str = "INFO") -> None:
"""Default logger if none is provided."""
print(f"[{level}] {message}")
def _log(self, message: str, level: str = "INFO") -> None:
"""Helper for logging messages with a consistent prefix."""
try:
self.logger(f"[SimpleScriptBuilder] {message}", level=level)
except TypeError:
self.logger(f"[{level}][SimpleScriptBuilder] {message}")
def _create_widgets(self) -> None:
"""Creates all widgets for the simple script builder interface."""
# Script selection frame
self.script_frame = ttk.LabelFrame(self.parent_frame, text="Python Script Selection", padding="10")
self.script_label = ttk.Label(self.script_frame, text="Select Python Script:")
self.script_entry = ttk.Entry(self.script_frame, textvariable=self.script_path_var, width=70)
self.script_browse_btn = ttk.Button(
self.script_frame,
text="Browse...",
command=self._browse_script
)
# Options frame
self.options_frame = ttk.LabelFrame(self.parent_frame, text="Build Options", padding="10")
self.onefile_check = ttk.Checkbutton(
self.options_frame,
text="Create single file executable (--onefile)",
variable=self.onefile_var
)
# Output directory frame
self.output_frame = ttk.LabelFrame(self.parent_frame, text="Output Directory", padding="10")
self.output_label = ttk.Label(self.output_frame, text="Output Directory:")
self.output_entry = ttk.Entry(self.output_frame, textvariable=self.output_dir_var, width=70)
self.output_browse_btn = ttk.Button(
self.output_frame,
text="Browse...",
command=self._browse_output_dir
)
# Action buttons frame
self.action_frame = ttk.Frame(self.parent_frame, padding="10")
self.build_btn = ttk.Button(
self.action_frame,
text="Build Executable",
command=self._start_build,
state="disabled"
)
self.open_folder_btn = ttk.Button(
self.action_frame,
text="Open Output Folder",
command=self._open_output_folder,
state="disabled"
)
def _layout_widgets(self) -> None:
"""Layouts all widgets in the parent frame."""
# Script frame
self.script_frame.pack(fill=tk.X, padx=5, pady=5)
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_browse_btn.grid(row=0, column=2, padx=5, pady=5)
self.script_frame.columnconfigure(1, weight=1)
# Options frame
self.options_frame.pack(fill=tk.X, padx=5, pady=5)
self.onefile_check.pack(anchor="w", padx=5, pady=5)
# Output frame
self.output_frame.pack(fill=tk.X, padx=5, pady=5)
self.output_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.output_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
self.output_browse_btn.grid(row=0, column=2, padx=5, pady=5)
self.output_frame.columnconfigure(1, weight=1)
# Action buttons
self.action_frame.pack(fill=tk.X, padx=5, pady=10)
self.build_btn.pack(side=tk.LEFT, padx=5)
self.open_folder_btn.pack(side=tk.LEFT, padx=5)
def _browse_script(self) -> None:
"""Opens a file dialog to select a Python script."""
file_path = filedialog.askopenfilename(
title="Select Python Script",
filetypes=[
("Python files", "*.py"),
("Python files (no console)", "*.pyw"),
("All files", "*.*")
],
parent=self.parent_frame
)
if file_path:
self.script_path_var.set(file_path)
self._log(f"Selected script: {file_path}", level="INFO")
# Auto-suggest output directory (same directory as script)
if not self.output_dir_var.get():
script_dir = str(pathlib.Path(file_path).parent)
self.output_dir_var.set(script_dir)
self._log(f"Auto-set output directory: {script_dir}", level="DEBUG")
self._update_build_button_state()
def _browse_output_dir(self) -> None:
"""Opens a dialog to select the output directory."""
dir_path = filedialog.askdirectory(
title="Select Output Directory",
parent=self.parent_frame
)
if dir_path:
self.output_dir_var.set(dir_path)
self._log(f"Selected output directory: {dir_path}", level="INFO")
self._update_build_button_state()
def _update_build_button_state(self) -> None:
"""Enables or disables the build button based on input validity."""
script_path = self.script_path_var.get()
output_dir = self.output_dir_var.get()
if script_path and pathlib.Path(script_path).is_file() and output_dir:
self.build_btn.config(state="normal")
else:
self.build_btn.config(state="disabled")
def _start_build(self) -> None:
"""Starts the build process in a separate thread."""
if self.is_building:
self._log("Build already in progress!", level="WARNING")
return
script_path = self.script_path_var.get()
output_dir = self.output_dir_var.get()
if not script_path or not pathlib.Path(script_path).is_file():
messagebox.showerror(
"Invalid Script",
"Please select a valid Python script file.",
parent=self.parent_frame
)
return
if not output_dir:
messagebox.showerror(
"Invalid Output Directory",
"Please select an output directory.",
parent=self.parent_frame
)
return
self.is_building = True
self.build_btn.config(state="disabled")
self._log("=" * 50, level="INFO")
self._log("STARTING SIMPLE SCRIPT BUILD", level="INFO")
self._log("=" * 50, level="INFO")
# Run build in a separate thread to avoid freezing the GUI
build_thread = threading.Thread(
target=self._run_build_process,
args=(script_path, output_dir),
daemon=True
)
build_thread.start()
def _run_build_process(self, script_path: str, output_dir: str) -> None:
"""
Runs the PyInstaller build process.
Args:
script_path: Path to the Python script to build.
output_dir: Directory where build and dist folders will be created.
"""
try:
script_path_obj = pathlib.Path(script_path)
output_dir_obj = pathlib.Path(output_dir)
# Create output directory if it doesn't exist
if not output_dir_obj.exists():
self._log(f"Creating output directory: {output_dir_obj}", level="INFO")
try:
output_dir_obj.mkdir(parents=True, exist_ok=True)
self._log(f"Output directory created successfully", level="INFO")
except Exception as dir_err:
self._log(f"Failed to create output directory: {dir_err}", level="ERROR")
error_msg = f"Cannot create output directory:\n{output_dir_obj}\n\nError: {dir_err}"
self.parent_frame.after(
0,
lambda err=error_msg: messagebox.showerror(
"Directory Creation Failed",
err,
parent=self.parent_frame
)
)
return
elif not output_dir_obj.is_dir():
error_msg = f"The specified path exists but is not a directory:\n{output_dir_obj}"
self._log(error_msg, level="ERROR")
self.parent_frame.after(
0,
lambda err=error_msg: messagebox.showerror(
"Invalid Path",
err,
parent=self.parent_frame
)
)
return
# Prepare PyInstaller command
cmd = [sys.executable, "-m", "PyInstaller"]
# Add onefile option if selected
if self.onefile_var.get():
cmd.append("--onefile")
self._log("Building as single file executable", level="INFO")
else:
self._log("Building as directory bundle", level="INFO")
# Set output directories
build_dir = output_dir_obj / "_build"
dist_dir = output_dir_obj / "_dist"
cmd.extend([
"--distpath", str(dist_dir),
"--workpath", str(build_dir),
"--specpath", str(output_dir_obj),
])
# Add the script path
cmd.append(str(script_path_obj))
self._log(f"Executing: {' '.join(cmd)}", level="DEBUG")
self._log(f"Working directory: {output_dir_obj}", level="DEBUG")
# Run PyInstaller
process = subprocess.Popen(
cmd,
cwd=str(output_dir_obj),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
# Stream output to logger
if process.stdout:
for line in process.stdout:
line = line.rstrip()
if line:
self._log(line, level="INFO")
process.wait()
# Check result
if process.returncode == 0:
self._log("=" * 50, level="INFO")
self._log("BUILD COMPLETED SUCCESSFULLY!", level="INFO")
self._log(f"Output location: {dist_dir}", level="INFO")
self._log("=" * 50, level="INFO")
# Enable open folder button
self.open_folder_btn.config(state="normal")
# Show success message in main thread
self.parent_frame.after(
0,
lambda d=str(dist_dir): messagebox.showinfo(
"Build Successful",
f"Executable built successfully!\n\nOutput: {d}",
parent=self.parent_frame
)
)
else:
self._log("=" * 50, level="ERROR")
self._log(f"BUILD FAILED! Exit code: {process.returncode}", level="ERROR")
self._log("=" * 50, level="ERROR")
# Show error message in main thread
self.parent_frame.after(
0,
lambda rc=process.returncode: messagebox.showerror(
"Build Failed",
f"Build process failed with exit code {rc}.\n\nCheck the log for details.",
parent=self.parent_frame
)
)
except Exception as e:
self._log(f"Error during build: {e}", level="ERROR")
import traceback
self._log(traceback.format_exc(), level="ERROR")
# Show error message in main thread
error_msg = str(e)
self.parent_frame.after(
0,
lambda err=error_msg: messagebox.showerror(
"Build Error",
f"An error occurred during the build:\n{err}",
parent=self.parent_frame
)
)
finally:
self.is_building = False
# Re-enable build button in main thread
self.parent_frame.after(0, self._update_build_button_state)
def _open_output_folder(self) -> None:
"""Opens the output folder in the system file explorer."""
output_dir = self.output_dir_var.get()
if not output_dir or not pathlib.Path(output_dir).is_dir():
messagebox.showerror(
"Invalid Directory",
"Output directory does not exist.",
parent=self.parent_frame
)
return
dist_dir = pathlib.Path(output_dir) / "_dist"
# Use the dist directory if it exists, otherwise use the output directory
folder_to_open = dist_dir if dist_dir.is_dir() else pathlib.Path(output_dir)
try:
if sys.platform == "win32":
os.startfile(str(folder_to_open))
elif sys.platform == "darwin":
subprocess.run(["open", str(folder_to_open)])
else:
subprocess.run(["xdg-open", str(folder_to_open)])
self._log(f"Opened folder: {folder_to_open}", level="INFO")
except Exception as e:
self._log(f"Failed to open folder: {e}", level="ERROR")
messagebox.showerror(
"Error",
f"Failed to open folder:\n{e}",
parent=self.parent_frame
)