diff --git a/pyinstallerguiwrapper/gui/main_window.py b/pyinstallerguiwrapper/gui/main_window.py index a6c7310..c8c210e 100644 --- a/pyinstallerguiwrapper/gui/main_window.py +++ b/pyinstallerguiwrapper/gui/main_window.py @@ -19,6 +19,7 @@ from .project_selector import ProjectManager from .options_manager import OptionsManager from .output_logger import OutputLogger from .data_files_manager import DataFilesManager +from .simple_script_builder import SimpleScriptBuilder from pyinstallerguiwrapper.build.version_manager import VersionManager from pyinstallerguiwrapper.build.build_orchestrator import BuildOrchestrator from pyinstallerguiwrapper import config @@ -57,6 +58,17 @@ class PyInstallerGUI(tk.Tk): self.rowconfigure(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.output_logger = OutputLogger( @@ -125,6 +137,11 @@ class PyInstallerGUI(tk.Tk): 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.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) def _create_widgets_frames_and_output_log_area(self) -> None: - # Questo metodo non ha bisogno di modifiche, lo ometto per brevità - self.project_frame = ttk.LabelFrame(self.main_frame, text="Project Directory") + # === PROJECT MODE WIDGETS (in project_mode_frame) === + 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_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.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_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:") @@ -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_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.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_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_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: - # 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.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.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) + + # Derived paths frame 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.columnconfigure(1, weight=1) 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_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") + + # Options notebook row_idx += 1 - self.main_frame.rowconfigure(row_idx, weight=0) - self.options_notebook.grid(row=row_idx, column=0, padx=5, pady=5, sticky="ew") + self.project_mode_frame.rowconfigure(row_idx, weight=1) + 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.rowconfigure(1, weight=1) 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_dir.config(command=self.data_files_manager.add_directory) self.data_button_remove.config(command=self.data_files_manager.remove_selected) + + # Action frame row_idx += 1 - self.main_frame.rowconfigure(row_idx, weight=1) - self.output_frame.grid(row=row_idx, column=0, padx=5, pady=5, sticky="nsew") + self.project_mode_frame.rowconfigure(row_idx, weight=0) + 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.columnconfigure(0, weight=1) 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: # Questo metodo non ha bisogno di modifiche, lo ometto per brevità diff --git a/pyinstallerguiwrapper/gui/simple_script_builder.py b/pyinstallerguiwrapper/gui/simple_script_builder.py new file mode 100644 index 0000000..dc8e1fb --- /dev/null +++ b/pyinstallerguiwrapper/gui/simple_script_builder.py @@ -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 + )