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