From f3a77fb7df63c6f1371a5ab47b4cd180a44bc3f6 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Wed, 14 May 2025 14:42:12 +0200 Subject: [PATCH] add backup ini and json file before create exec, add flag no-clean for pyinstaller --- pyinstallerguiwrapper.spec | 18 +- pyinstallerguiwrapper/_version.py | 23 +- pyinstallerguiwrapper/builder.py | 78 +++- pyinstallerguiwrapper/config.py | 4 +- pyinstallerguiwrapper/gui.py | 713 ++++++++++++++---------------- 5 files changed, 394 insertions(+), 442 deletions(-) diff --git a/pyinstallerguiwrapper.spec b/pyinstallerguiwrapper.spec index bec373a..935ab56 100644 --- a/pyinstallerguiwrapper.spec +++ b/pyinstallerguiwrapper.spec @@ -2,7 +2,6 @@ block_cipher = None -import os a = Analysis(scripts=['pyinstallerguiwrapper\\__main__.py'], pathex=['pyinstallerguiwrapper'], binaries=[], @@ -21,26 +20,17 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=None) exe = EXE(pyz, a.scripts, - [], # Binaries/Datas usually handled by Analysis/COLLECT - exclude_binaries=True, # Let COLLECT handle binaries in one-dir + [], + exclude_binaries=True, name='PyInstallerGUIWrapper', debug=False, bootloader_ignore_signals=False, strip=False, - upx=True, # Use UPX based on config + upx=True, runtime_tmpdir=None, - console=False, # Set console based on GUI checkbox + console=False, disable_windowed_traceback=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon='pyinstallerguiwrapper.ico') - -coll = COLLECT(exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, # Match UPX setting - upx_exclude=[], - name='PyInstallerGUIWrapper') diff --git a/pyinstallerguiwrapper/_version.py b/pyinstallerguiwrapper/_version.py index b5b28c4..e9547b5 100644 --- a/pyinstallerguiwrapper/_version.py +++ b/pyinstallerguiwrapper/_version.py @@ -8,10 +8,10 @@ import re # --- Version Data (Generated) --- # This section is automatically generated by the build process. -__version__ = "v.0.0.0.3-4-gf671da0-dirty" -GIT_COMMIT_HASH = "f671da08f3066a1f5d2ff877ebc28a9155903477" +__version__ = "v.0.0.0.3-5-g9061dcf-dirty" +GIT_COMMIT_HASH = "9061dcfb502fe1838aff1553f1efb80816f57c8c" GIT_BRANCH = "master" -BUILD_TIMESTAMP = "2025-05-08T08:56:28Z" +BUILD_TIMESTAMP = "2025-05-14T12:40:23Z" IS_GIT_REPO = True # --- Default Values (for comparison or fallback) --- @@ -42,11 +42,10 @@ def get_version_string(format_string=None): str: The formatted version string, or an error message if formatting fails. """ if format_string is None: - format_string = "{version} ({branch}/{commit_short})" # Sensible default + format_string = "{version} ({branch}/{commit_short})" replacements = {} try: - # Prepare data dictionary for substitution 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 @@ -56,35 +55,23 @@ def get_version_string(format_string=None): replacements['is_git'] = "Git" if IS_GIT_REPO else "Unknown" replacements['dirty'] = "-dirty" if __version__ and __version__.endswith('-dirty') else "" - # Extract clean tag using regex (handles versions like v1.0.0, 1.0.0) tag = DEFAULT_VERSION if __version__ and IS_GIT_REPO: - # Match optional 'v' prefix, then major.minor.patch match = re.match(r'^(v?([0-9]+)\.([0-9]+)\.([0-9]+))', __version__) if match: - tag = match.group(1) # Get the full tag (e.g., 'v1.0.0') + tag = match.group(1) replacements['tag'] = tag - # Perform substitution using regex to find placeholders {placeholder} output_string = format_string - # Iterate through placeholders and replace them in the format string for placeholder, value in replacements.items(): - # Compile regex pattern for {placeholder}, allowing for whitespace inside braces pattern = re.compile(r'{\s*' + re.escape(placeholder) + r'\s*}') - # Substitute found patterns with the corresponding string value output_string = pattern.sub(str(value), output_string) - # Optional: Check if any placeholders remain unsubstituted (could indicate typo) if re.search(r'{\s*[\w_]+\s*}', output_string): - # You might want to log this or handle it, for now, we return the string as is - # print(f"Warning: Unsubstituted placeholders remain in version string: {output_string}") pass return output_string except Exception as e: - # Return a simple error message in case of unexpected formatting issues - # Avoid printing directly from this generated function return f"[Formatting Error: {e}]" - diff --git a/pyinstallerguiwrapper/builder.py b/pyinstallerguiwrapper/builder.py index f491cf8..a23e653 100644 --- a/pyinstallerguiwrapper/builder.py +++ b/pyinstallerguiwrapper/builder.py @@ -7,61 +7,109 @@ import queue import os import traceback # For logging exceptions -# NO CHANGES NEEDED in this file for the restructuring +# NO CHANGES NEEDED in this file for the restructuring - EXCEPT FOR THE DEBUG LOG LINE ADDED BELOW def run_build_in_thread(command, working_dir, output_queue, logger_func, output_dir_name, environment=None): """ Executes the PyInstaller command using subprocess.Popen in the background. - ... (docstring remains the same) ... + Streams stdout/stderr to the output_queue for display in the GUI. + Notifies the main thread of success, failure, or completion via the queue. + + Args: + command (list): The PyInstaller command and its arguments as a list of strings. + working_dir (str): The directory from which to run the command (project root). + output_queue (queue.Queue): Queue to send log messages and status updates. + logger_func (function): Function from the GUI to log messages (also used by this thread). + output_dir_name (str): The name of the dist/output directory (e.g., '_dist'). + environment (dict, optional): A custom environment for the subprocess. Defaults to None (inherited). """ - # ... (Function body remains the same as the previously translated version) ... build_process = None try: logger_func("Build thread starting execution...", level="INFO") + + # --- MODIFICA INIZIO --- + # Loggare il comando esatto che sta per essere eseguito per debugging + # Questo ci aiuterà a vedere se --clean è presente o meno. + # Usiamo una formattazione che gestisca gli spazi negli argomenti per una migliore leggibilità del log. + command_str_for_log = " ".join([f'"{c}"' if " " in c else c for c in command]) + logger_func(f"PyInstaller command to be executed: {command_str_for_log}", level="DEBUG") + logger_func(f"Working directory: {working_dir}", level="DEBUG") + # --- MODIFICA FINE --- + env_log_msg = f"Using {'custom' if environment else 'inherited'} environment." - if environment: pass + if environment: pass # Placeholder, si potrebbe loggare di più sull'ambiente se necessario logger_func(f"PyInstaller process starting. {env_log_msg}", level="INFO") + build_process = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=working_dir, - text=True, encoding='utf-8', errors='replace', bufsize=1, env=environment ) + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Redirect stderr to stdout + cwd=working_dir, + text=True, # Decode output as text + encoding='utf-8', + errors='replace', # Handle potential decoding errors gracefully + bufsize=1, # Line-buffered + env=environment # Pass the custom or inherited environment + ) logger_func(f"PyInstaller process launched (PID: {build_process.pid}).", level="INFO") + + # Stream output while True: try: line = build_process.stdout.readline() if not line: + # Check if process has ended if build_process.poll() is not None: logger_func("End of PyInstaller output stream.", level="DEBUG") break - else: pass - else: output_queue.put(("LOG", line)) + else: + # Process might still be running but not producing output momentarily + # Could add a small sleep here if CPU usage is a concern, + # but for typical PyInstaller builds, continuous checking is fine. + pass + else: + output_queue.put(("LOG", line)) # Send line to GUI for display except Exception as read_err: + # This might happen if the stream is closed unexpectedly logger_func(f"Error reading PyInstaller output stream: {read_err}\n{traceback.format_exc()}", level="ERROR") break - return_code = build_process.wait() + + return_code = build_process.wait() # Wait for the process to complete logger_func(f"PyInstaller process finished with exit code: {return_code}", level="INFO") + if return_code == 0: - dist_path_abs = os.path.join(working_dir, output_dir_name) - if os.path.exists(dist_path_abs): + # Check if the expected output directory/file exists + dist_path_abs = os.path.join(working_dir, output_dir_name) # output_dir_name is like '_dist' + # Note: For one-file, the output is a file, not a directory directly under dist_path_abs + # For one-dir, the output is a directory. + # We check for the existence of the dist_path_abs itself. + # A more robust check might involve looking for the executable name inside dist_path_abs. + if os.path.exists(dist_path_abs): # This checks if '_dist' (or equivalent) exists success_msg = f"Build completed successfully!\nOutput Directory: {dist_path_abs}" output_queue.put(("BUILD_SUCCESS", success_msg)) else: + # This case might be rare if PyInstaller exits with 0, but good for robustness warn_msg = (f"Build finished with exit code 0, but output directory not found:\n" f"{dist_path_abs}\nCheck the full log for potential issues.") logger_func(warn_msg, level="WARNING") - output_queue.put(("BUILD_ERROR", warn_msg)) + output_queue.put(("BUILD_ERROR", warn_msg)) # Treat as error if output dir is missing else: error_msg = f"Build failed! (Exit Code: {return_code})\nCheck the log for details." output_queue.put(("BUILD_ERROR", error_msg)) + except FileNotFoundError: + # This occurs if 'pyinstaller' command itself is not found error_msg = ("Error: 'pyinstaller' command not found.\n" "Ensure PyInstaller is installed correctly and in the system PATH.") output_queue.put(("BUILD_ERROR", error_msg)) - logger_func(error_msg, level="CRITICAL") + logger_func(error_msg, level="CRITICAL") # Logged via logger_func as well except Exception as e: + # Catch any other unexpected errors during process execution error_msg = f"Unexpected error during build process execution: {e}" output_queue.put(("BUILD_ERROR", error_msg)) - logger_func(error_msg, level="CRITICAL") - logger_func(traceback.format_exc(), level="DEBUG") + logger_func(error_msg, level="CRITICAL") # Logged via logger_func + logger_func(traceback.format_exc(), level="DEBUG") # Log full traceback for debugging finally: + # Always signal that the build attempt (successful or not) has finished output_queue.put(("BUILD_FINISHED", None)) logger_func("Build thread finished.", level="INFO") \ No newline at end of file diff --git a/pyinstallerguiwrapper/config.py b/pyinstallerguiwrapper/config.py index 9e40c76..3a28aed 100644 --- a/pyinstallerguiwrapper/config.py +++ b/pyinstallerguiwrapper/config.py @@ -3,8 +3,6 @@ Configuration constants for the PyInstaller GUI Wrapper application. """ -# NO CHANGES NEEDED in this file for the restructuring - # Default values for PyInstaller options managed by the GUI DEFAULT_SPEC_OPTIONS = { 'onefile': False, @@ -14,7 +12,7 @@ DEFAULT_SPEC_OPTIONS = { 'add_data': [], 'hidden_imports': [], 'log_level': 'INFO', - 'clean_build': True, # Corresponds to --clean + 'clean_build': False, # MODIFIED: Default is now False. Corresponds to --clean 'confirm_overwrite': False,# Corresponds to --noconfirm (True means --noconfirm is active) 'output_dir_name': '_dist', # Default name for the output directory 'work_dir_name': '_build', # Default name for the temporary build directory diff --git a/pyinstallerguiwrapper/gui.py b/pyinstallerguiwrapper/gui.py index 1bb0e8a..be37efc 100644 --- a/pyinstallerguiwrapper/gui.py +++ b/pyinstallerguiwrapper/gui.py @@ -25,8 +25,6 @@ try: 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: - # This might happen if you run the wrapper directly from source - # without generating its _version.py first (if you use that approach for the wrapper itself) WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)" WRAPPER_BUILD_INFO = "Wrapper build time unknown" # --- End Import Version Info --- @@ -47,25 +45,28 @@ class PyInstallerGUI(tk.Tk): def __init__(self): """ Initialize the main application window, variables, and widgets. """ super().__init__() - # Use the Wrapper's version in the title self.title(f"PyInstaller GUI Wrapper - {WRAPPER_APP_VERSION_STRING}") # --- Project Path --- - self.project_directory_path = tk.StringVar() # Stores the selected main project folder + 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 # Will store a Path object - self._project_root_name = "" # e.g., "MyTool" + 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() # Holds the DERIVED or user-overridden icon path - self.app_name = tk.StringVar() # Derived or overridden in GUI/spec + 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 = [] @@ -83,45 +84,38 @@ class PyInstallerGUI(tk.Tk): self._create_widgets(self.main_frame) self._layout_widgets() - # Start monitoring the build queue self.after(100, self._check_build_queue) self._log_to_gui(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") - # --- Helper Method for Running Git Commands --- 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: - # Execute command in the target directory process = subprocess.run( command, - shell=True, # Use shell=True cautiously, ok here as command is internal + shell=True, capture_output=True, text=True, - cwd=target_dir, # CRITICAL: Run in the target project's directory - check=False, # Don't raise exception on non-zero exit code - errors='ignore' # Handle potential decoding errors + cwd=target_dir, + check=False, + errors='ignore' ) if process.returncode == 0: - return process.stdout.strip() # Return successful output + return process.stdout.strip() else: - # Log failures but don't make version generation fatal unless necessary 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: - # Log if git command itself is not found self._log_to_gui("Git command failed: 'git' not found. Is Git installed and in PATH?", level="ERROR") - return None # Indicate git is likely missing + return None except Exception as e: - # Log other potential errors during subprocess execution 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 - # --- Method to Generate Target Project's _version.py --- def _generate_target_version_file(self, project_dir, target_version_file_path): """ Checks if project_dir is a git repo, gathers version info, @@ -131,48 +125,33 @@ class PyInstallerGUI(tk.Tk): """ self._log_to_gui(f"Attempting to generate version file for target: {target_version_file_path}", level="INFO") - # Initialize with default values version_info = { "version": DEFAULT_VERSION, "commit": DEFAULT_COMMIT, "branch": DEFAULT_BRANCH, } is_git_repo = False - - # Check if the target directory is a git repository git_check = self._run_git_command_in_dir("git rev-parse --is-inside-work-tree", project_dir) if git_check == "true": - # If it is a Git repo, try to fetch version details is_git_repo = True self._log_to_gui(f"Project directory '{project_dir}' identified as a Git repository.", level="INFO") - - # Get version string (tag, or commit if no tag, plus dirty status) describe_output = self._run_git_command_in_dir("git describe --tags --always --dirty", project_dir) if describe_output: version_info["version"] = describe_output - - # Get full commit hash commit_output = self._run_git_command_in_dir("git rev-parse HEAD", project_dir) if commit_output: version_info["commit"] = commit_output - - # Get current branch name, handling detached HEAD 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]}" # Show short commit in detached state + version_info["branch"] = f"detached@{version_info['commit'][:7]}" else: - # Log if not a git repo or if the check failed self._log_to_gui(f"Project directory '{project_dir}' is not a Git repository or Git failed. Using default version info.", level="WARNING") - # Get current timestamp for the build timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") - # --- Define the Function to be Embedded in _version.py --- - # MODIFIED: Use raw f-string (rf''') to avoid SyntaxWarnings for '\s' - # MODIFIED: Escape braces in the comment: {{word}} get_version_function_code = rf''' import re @@ -212,11 +191,10 @@ def get_version_string(format_string=None): str: The formatted version string, or an error message if formatting fails. """ if format_string is None: - format_string = "{{version}} ({{branch}}/{{commit_short}})" # Sensible default + format_string = "{{version}} ({{branch}}/{{commit_short}})" replacements = {{}} try: - # Prepare data dictionary for substitution 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 @@ -226,40 +204,26 @@ def get_version_string(format_string=None): replacements['is_git'] = "Git" if IS_GIT_REPO else "Unknown" replacements['dirty'] = "-dirty" if __version__ and __version__.endswith('-dirty') else "" - # Extract clean tag using regex (handles versions like v1.0.0, 1.0.0) tag = DEFAULT_VERSION if __version__ and IS_GIT_REPO: - # Match optional 'v' prefix, then major.minor.patch match = re.match(r'^(v?([0-9]+)\.([0-9]+)\.([0-9]+))', __version__) if match: - tag = match.group(1) # Get the full tag (e.g., 'v1.0.0') + tag = match.group(1) replacements['tag'] = tag - # Perform substitution using regex to find placeholders {{placeholder}} output_string = format_string - # Iterate through placeholders and replace them in the format string for placeholder, value in replacements.items(): - # Compile regex pattern for {{placeholder}}, allowing for whitespace inside braces pattern = re.compile(r'{{\s*' + re.escape(placeholder) + r'\s*}}') - # Substitute found patterns with the corresponding string value output_string = pattern.sub(str(value), output_string) - # Optional: Check if any placeholders remain unsubstituted (could indicate typo) if re.search(r'{{\s*[\w_]+\s*}}', output_string): - # You might want to log this or handle it, for now, we return the string as is - # print(f"Warning: Unsubstituted placeholders remain in version string: {{output_string}}") pass return output_string except Exception as e: - # Return a simple error message in case of unexpected formatting issues - # Avoid printing directly from this generated function return f"[Formatting Error: {{e}}]" - ''' - # --- Combine Header and Function Code --- - # Standard header for generated files content = f'''\ # -*- coding: utf-8 -*- # File generated by PyInstaller GUI Wrapper. DO NOT EDIT MANUALLY. @@ -268,31 +232,24 @@ def get_version_string(format_string=None): {get_version_function_code} ''' - # --- Write the _version.py file --- try: - # Ensure the target directory (source folder of the project being built) exists os.makedirs(os.path.dirname(target_version_file_path), exist_ok=True) - # Write the generated content to the file with open(target_version_file_path, 'w', encoding='utf-8') as f: f.write(content) - # Log success self._log_to_gui(f"Successfully generated/updated target version file (with function): {target_version_file_path}", level="INFO") - # Log the default formatted output for easy verification in the GUI log 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 # Indicate success + return True except Exception as e: - # Log critical error if writing fails and show an error to the user 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 # Indicate failure + return False - # Helper just for logging the default format output during generation 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 @@ -300,8 +257,6 @@ def get_version_string(format_string=None): c = version_info['commit'][:7] if version_info['commit'] and len(version_info['commit']) >= 7 else DEFAULT_COMMIT return f"{v} ({b}/{c})" - - # --- Widget Creation --- def _create_widgets(self, parent_frame): # --- Section 1: Input Project Directory --- self.project_frame = ttk.LabelFrame(parent_frame, text="Project Directory") @@ -329,6 +284,12 @@ def get_version_string(format_string=None): 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)") @@ -352,29 +313,25 @@ def get_version_string(format_string=None): self.action_frame = ttk.Frame(parent_frame) self.build_button = ttk.Button(self.action_frame, text="Build Executable", command=self._start_build, state="disabled") - # --- Layout Widgets --- def _layout_widgets(self): - # Row configuration row_idx = 0 - self.main_frame.rowconfigure(row_idx, weight=0) # Project Dir + self.main_frame.rowconfigure(row_idx, weight=0) row_idx += 1 - self.main_frame.rowconfigure(row_idx, weight=0) # Derived Paths + self.main_frame.rowconfigure(row_idx, weight=0) row_idx += 1 - self.main_frame.rowconfigure(row_idx, weight=0) # Notebook row + self.main_frame.rowconfigure(row_idx, weight=0) row_idx += 1 - self.main_frame.rowconfigure(row_idx, weight=1) # Output log expands + self.main_frame.rowconfigure(row_idx, weight=1) row_idx += 1 - self.main_frame.rowconfigure(row_idx, weight=0) # Action buttons + self.main_frame.rowconfigure(row_idx, weight=0) self.main_frame.columnconfigure(0, weight=1) - # Layout Project Directory Selection 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) - # Layout Derived Paths Display 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) @@ -384,20 +341,26 @@ def get_version_string(format_string=None): 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") - # Layout Options Notebook row_idx += 1 self.options_notebook.grid(row=row_idx, column=0, padx=5, pady=5, sticky="ew") - # --- Layout Basic Options Tab --- + self.basic_options_frame.columnconfigure(1, weight=1) - self.name_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") - self.name_entry.grid(row=0, column=1, columnspan=3, padx=5, pady=5, sticky="ew") - self.icon_label.grid(row=1, column=0, padx=5, pady=5, sticky="w") - self.icon_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew") - self.icon_button_browse.grid(row=1, column=2, padx=(0, 2), pady=5) - self.icon_button_clear.grid(row=1, column=3, padx=(2, 5), pady=5) - self.onefile_check.grid(row=2, column=0, columnspan=4, padx=5, pady=5, sticky="w") - self.windowed_check.grid(row=3, column=0, columnspan=4, padx=5, pady=5, sticky="w") - # --- Layout Data Files Tab --- + 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") @@ -408,19 +371,16 @@ def get_version_string(format_string=None): self.data_button_add_file.pack(pady=2, fill=tk.X) self.data_button_add_dir.pack(pady=2, fill=tk.X) self.data_button_remove.pack(pady=2, fill=tk.X) - # Layout Build Output Log + 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") - # Layout Action Frame 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) - # --- Callback Methods --- def _select_project_directory(self): - """ Handles project directory selection, derives paths, checks structure, loads spec. """ 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: @@ -434,24 +394,28 @@ def get_version_string(format_string=None): self._project_root_name = project_root.name project_name_lower = self._project_root_name.lower() - # Store derived paths as pathlib objects 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" + 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" + - # Log derived paths 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") - # Validate structure and update GUI labels valid_structure = True missing_files = [] - # Check main script if self._derived_main_script_path.is_file(): self.derived_script_label_val.config(text=str(self._derived_main_script_path), foreground="black") else: @@ -459,414 +423,353 @@ def get_version_string(format_string=None): missing_files.append(f"Main script ({self._derived_main_script_path.name})") valid_structure = False - # Check spec file 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!", foreground="red") - missing_files.append(f"Spec file ({self._derived_spec_path.name})") - valid_structure = False # Spec file is considered essential + 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. - # Check icon file (optional) 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)) # Pre-fill GUI + 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("") # Clear GUI field if not found + self.icon_path.set("") - # Check source directory existence (important for writing _version.py) 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") - # Consider if this should be treated as an error preventing build - # For now, we let the check in _start_build handle it if critical - - # Reset options and try loading the spec file if found + + # 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") - # Set default app name if spec doesn't exist to provide one - self.app_name.set(self._project_root_name) + self.app_name.set(self._project_root_name) # Default app name if no spec - # Enable/Disable Build Button based on essential file existence - # We require main script and spec file for build to be possible. - if valid_structure: + # 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 structure valid. Review options or click 'Build Executable'.", level="INFO") + 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: Invalid project structure or missing essential files:\n- " + "\n- ".join(missing_files) + 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): - """ Checks for and parses an existing spec file using spec_parser. """ 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") - # Pass project root for context if needed by parser (for resolving relative paths) parsed_options = spec_parser.parse_spec_file(spec_path, logger_func=self._log_to_gui) if parsed_options is not None: if parsed_options: - # Update GUI based on parsed options, providing project root for path resolution 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 overwrite the spec.") + 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: - # Parsing failed - messagebox.showerror("Spec File Parsing Error", f"Failed to parse spec file:\n{spec_path}\n\nCheck log. Proceeding will overwrite the spec.") - + 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): - """ Resets GUI option widgets and internal data lists to defaults. """ self._log_to_gui("Resetting options to default values.", level="INFO") default_app_name = "" - # Derive name from project directory if flag is set and path is available if derive_name and self._project_root_name: default_app_name = self._project_root_name - # Set name: use derived, fallback to config default 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 - # Icon path is handled during directory selection/spec parsing/clearing - # self.icon_path.set(...) should not be reset here blindly - - # Reset basic flags/dropdowns to config defaults 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']) - # Reset data list and listbox 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): - """ Updates GUI widgets AND data lists based on parsed options from spec file. """ self._log_to_gui("Updating GUI from loaded/parsed spec options.", level="INFO") - # Update basic options from parsed spec data if available if 'name' in options and options['name'] is not None: self.app_name.set(options['name']) - # Icon: Resolve path relative to project root if needed and check existence 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") - else: - abs_icon_path = icon_spec_path - # Only set GUI field if the resolved path actually exists + 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 'icon' not in spec, self.icon_path retains its value (derived or cleared) - - # Update flags if present and valid type in spec + 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']) - # We don't currently parse log_level from spec, but could add it here if needed + # Note: 'clean_build' is not typically in a spec file, so it's not updated from 'options'. - # --- Update Added Data list/listbox from 'datas' in spec --- self.added_data_list.clear() self.data_listbox.delete(0, tk.END) - # Use .get() with default empty list for safety 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: - # Validate format: tuple/list of 2 strings if isinstance(item, (tuple, list)) and len(item) == 2: - source_rel, destination = item - if isinstance(source_rel, str) and isinstance(destination, str): - # Store the relative source path (relative to spec file/project root) - self.added_data_list.append((source_rel, destination)) - # Display the relative source path in the listbox - display_text = f"{source_rel} -> {destination}" + 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) - # Log absolute path for verification (optional) - abs_source_check = project_root_path / source_rel - self._log_to_gui(f" Added data entry from spec: '{source_rel}' (abs: {abs_source_check}) -> '{destination}'", level="DEBUG") + 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: - # Log if elements are not strings self._log_to_gui(f" Ignored invalid 'datas' entry (non-strings): {item}", level="WARNING") else: - # Log if format is incorrect 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: - # Log if no 'datas' found or the list was empty/invalid 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): - """ Handles icon file selection based on platform. Overrides derived/spec icon. """ project_dir = self.project_directory_path.get() - initial_dir = project_dir if project_dir else None # Start browsing from project dir + initial_dir = project_dir if project_dir else None - # Get platform-specific file types from config 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 - # Show file dialog file_path = filedialog.askopenfilename(title="Select Application Icon (Overrides)", filetypes=filetypes, defaultextension=default_ext, initialdir=initial_dir) if file_path: - # Update GUI if a file was selected self.icon_path.set(file_path) self._log_to_gui(f"Icon selected manually (overrides): {file_path}", level="INFO") - def _clear_icon(self): - """ Clears the icon selection in the GUI. """ self.icon_path.set("") self._log_to_gui("Icon selection cleared.") - # Update the derived icon display label to reflect the change if self._derived_icon_path and pathlib.Path(self._derived_icon_path).is_file(): - # If derived exists, show it again self.derived_icon_label_val.config(text=str(self._derived_icon_path), foreground="black") else: - # Otherwise show 'not found' self.derived_icon_label_val.config(text="Not found (optional)", foreground="grey") - def _add_data_file(self): - """ Adds a selected file to the 'add_data' list, storing source relative to project root. """ project_dir = self.project_directory_path.get() if not project_dir: messagebox.showerror("Error", "Select the project directory first."); return - # Show file dialog, starting in project directory 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: - # Resolve paths and calculate relative path abs_source_path = pathlib.Path(source_path).resolve() project_root_path = pathlib.Path(project_dir).resolve() - # Calculate source path relative to project root (for spec file) relative_source_path = os.path.relpath(str(abs_source_path), str(project_root_path)) - self._log_to_gui(f"Absolute source path: {abs_source_path}", level="DEBUG") - self._log_to_gui(f"Project root path: {project_root_path}", level="DEBUG") - self._log_to_gui(f"Calculated relative source path: {relative_source_path}", level="DEBUG") - + self._log_to_gui(f"Calculated relative source path for spec: {relative_source_path}", level="DEBUG") except ValueError as e: - # Handle error if paths are on different drives (Windows) 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: - # Handle other unexpected path processing errors messagebox.showerror("Error", f"Unexpected error processing path: {e}") self._log_to_gui(f"Error processing path '{source_path}': {e}", level="ERROR") return - # Ask for destination path within the bundle dest_path = simpledialog.askstring("Destination in Bundle", - f"Relative destination path for:\n{abs_source_path.name}\n(e.g., '.', 'data', 'images/subdir')", + f"Relative destination path for:\n{abs_source_path.name}\n(e.g., '.', 'data', 'assets/file.txt')", initialvalue=".") if dest_path is None: - # User cancelled the destination dialog self._log_to_gui("Add file cancelled (destination).", level="DEBUG"); return - # Add entry to internal list and GUI listbox - entry = (relative_source_path, dest_path) + entry = (relative_source_path, dest_path) # Store path relative to project root self.added_data_list.append(entry) - # Display the relative source path in the listbox for clarity 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") - + self._log_to_gui(f"Added data file: {display_text} (Source relative to project dir)", level="INFO") def _add_data_directory(self): - """ Adds a selected directory to the 'add_data' list, storing source relative to project root. """ project_dir = self.project_directory_path.get() if not project_dir: messagebox.showerror("Error", "Select the project directory first."); return - # Show directory dialog, starting in project directory 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: - # Resolve paths and calculate relative path abs_source_path = pathlib.Path(source_path).resolve() project_root_path = pathlib.Path(project_dir).resolve() - # Calculate source path relative to project root (for spec file) relative_source_path = os.path.relpath(str(abs_source_path), str(project_root_path)) - self._log_to_gui(f"Absolute source path: {abs_source_path}", level="DEBUG") - self._log_to_gui(f"Project root path: {project_root_path}", level="DEBUG") - self._log_to_gui(f"Calculated relative source path: {relative_source_path}", level="DEBUG") - + self._log_to_gui(f"Calculated relative source path for spec: {relative_source_path}", level="DEBUG") except ValueError as e: - # Handle error if paths are on different drives (Windows) 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: - # Handle other unexpected path processing errors messagebox.showerror("Error", f"Unexpected error processing path: {e}") self._log_to_gui(f"Error processing path '{source_path}': {e}", level="ERROR") return - # Ask for destination path within the bundle, suggesting folder name as default - default_dest = abs_source_path.name # Default destination is the folder name itself + 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: - # User cancelled the destination dialog self._log_to_gui("Add folder cancelled (destination).", level="DEBUG"); return - # Add entry to internal list and GUI listbox - entry = (relative_source_path, dest_path) + entry = (relative_source_path, dest_path) # Store path relative to project root self.added_data_list.append(entry) - # Display the relative source path in the listbox for clarity 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") - + self._log_to_gui(f"Added data folder: {display_text} (Source relative to project dir)", level="INFO") def _remove_selected_data(self): - """ Removes the selected item from the 'add_data' list and listbox. """ selected_indices = self.data_listbox.curselection() if not selected_indices: - # Show warning if nothing is selected messagebox.showwarning("No Selection", "Select an item to remove."); return - # Get index and text of the selected item index = selected_indices[0] item_text = self.data_listbox.get(index) - # Remove from listbox self.data_listbox.delete(index) - # Remove from internal data list, checking index boundary for safety if index < len(self.added_data_list): - removed_entry = self.added_data_list.pop(index) + self.added_data_list.pop(index) self._log_to_gui(f"Removed data entry: {item_text}", level="INFO") else: - # Log error if index is out of sync (should not happen normally) 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") - - # --- Logging and Queue Handling --- def _log_to_gui(self, message, level="INFO"): - """ Appends a formatted message to the output log area safely via queue if needed. """ formatted_message = f"[{level}] {str(message).strip()}\n" - # If called from main thread, update directly; otherwise, put in queue if threading.current_thread() is threading.main_thread(): self._update_output_log_widget(formatted_message) else: - # Use queue for thread-safe communication with the GUI thread self.build_queue.put(("LOG", formatted_message)) - def _update_output_log_widget(self, message): - """ Internal method to append text to the ScrolledText widget (GUI thread ONLY). """ try: - # Enable widget, insert text, scroll to end, disable widget self.output_text.config(state="normal") self.output_text.insert(tk.END, message) - self.output_text.see(tk.END) # Auto-scroll + self.output_text.see(tk.END) self.output_text.config(state="disabled") - self.output_text.update_idletasks() # Ensure GUI updates immediately + self.output_text.update_idletasks() except Exception as e: - # Print error to console if GUI logging fails print(f"[ERROR] Error updating log widget: {e}\n{traceback.format_exc()}") - def _check_build_queue(self): - """ Periodically checks the build queue for messages from the builder thread. """ try: - # Process all available messages in the queue without blocking while True: - # Get item non-blockingly item = self.build_queue.get_nowait() if isinstance(item, tuple) and len(item) == 2: - command, data = item - # Process different message types - if command == "LOG": + command_type, data = item # Rinominato 'command' in 'command_type' per chiarezza + + if command_type == "LOG": self._update_output_log_widget(data) - elif command == "BUILD_SUCCESS": - self._log_to_gui(data, level="SUCCESS") + elif command_type == "BUILD_SUCCESS": + self._log_to_gui(data, level="SUCCESS") # data è il success_msg messagebox.showinfo("Build Successful", data) - self._on_build_finished() # Update GUI state after success - elif command == "BUILD_ERROR": - self._log_to_gui(data, level="ERROR") + + # --- 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._on_build_finished() # Update GUI state after error - elif command == "BUILD_FINISHED": - # Signal that the build process itself (success or fail) has ended + 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") - self._on_build_finished() # Ensure state is reset finally + # Non pulire qui, la pulizia avviene dopo BUILD_SUCCESS o BUILD_ERROR + self._on_build_finished() else: - # Log unknown commands received via the queue - self._log_to_gui(f"Unknown command from queue: {command}", level="WARNING") + self._log_to_gui(f"Unknown command from queue: {command_type}", level="WARNING") else: - # Log unexpected item format in the queue self._log_to_gui(f"Unexpected item in queue: {item}", level="WARNING") - # Mark task as done for queue management self.build_queue.task_done() except queue.Empty: - # Queue is empty, nothing more to process right now pass except Exception as e: - # Log errors encountered while processing the queue itself 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: - # Reschedule the check after a short delay (e.g., 100ms) 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): - """ Actions to take when build completes (success or failure). Resets build state. """ - # Check if a valid project is still selected and essential files exist project_dir = self.project_directory_path.get() - can_build = (project_dir and - self._derived_main_script_path and pathlib.Path(self._derived_main_script_path).is_file() and - self._derived_spec_path) # We rely on spec existing or being generated + can_build_again = (project_dir and + self._derived_main_script_path and + pathlib.Path(self._derived_main_script_path).is_file()) - # Spec file existence check might be redundant if _start_build always saves it, - # but keep it for robustness in case saving fails silently or file is removed. - spec_file_exists = self._derived_spec_path and pathlib.Path(self._derived_spec_path).is_file() - - # Re-enable build button only if conditions are met - self.build_button.config(state="normal" if can_build and spec_file_exists else "disabled") - - # Clear the build thread reference + 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 (re-)enabled/disabled based on project state.", level="INFO") - + 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): - """ Generates the .spec file content based on GUI options, using derived paths. """ project_dir = self.project_directory_path.get() - # Validate essential inputs 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() - # Calculate script path relative to project root try: script_rel_path = os.path.relpath(str(pathlib.Path(self._derived_main_script_path).resolve()), str(project_root_path)) except Exception as e: @@ -874,45 +777,42 @@ def get_version_string(format_string=None): messagebox.showerror("Path Error", f"Cannot calculate relative path for the main script:\n{e}") return None - # Calculate source directory path relative to project root, fallback to '.' try: - source_dir_abs = pathlib.Path(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") + # 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_rel_path = os.path.relpath(str(source_dir_abs), str(project_root_path)) + 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 = "." - - # Get app name from GUI or derive from project name + app_name_val = self.app_name.get() or self._project_root_name - # Get icon path from GUI and make it relative if valid 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 in generated spec.", level="WARNING") - # icon_rel_path remains None + self._log_to_gui(f"Cannot make icon path relative '{icon_gui_path}': {e}. Icon will not be included.", level="WARNING") - # Helper to escape backslashes for Python strings in the spec file def escape_path(p): return p.replace("\\", "\\\\") if p else "" - # --- Analysis Section --- - # Use relative paths within the spec file context analysis_scripts = f"['{escape_path(script_rel_path)}']" - # Pathex should include the source directory relative to the spec file - analysis_pathex = f"['{escape_path(source_dir_rel_path)}']" - # 'datas' uses relative paths already stored in self.added_data_list + 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).", level="DEBUG") - # Placeholders for other Analysis options - formatted_hiddenimports = "[]"; formatted_binaries = "[]" + 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 - # Construct the Analysis block string analysis_str = f"""a = Analysis(scripts={analysis_scripts}, pathex={analysis_pathex}, binaries={formatted_binaries}, @@ -927,202 +827,231 @@ def get_version_string(format_string=None): cipher=None, noarchive=False)""" - # --- PYZ Section --- pyz_str = "pyz = PYZ(a.pure, a.zipped_data, cipher=None)" - # --- EXE Section --- - # Construct the EXE block string with options from GUI/defaults exe_str = f"""exe = EXE(pyz, a.scripts, - [], # Binaries/Datas usually handled by Analysis/COLLECT - exclude_binaries=True, # Let COLLECT handle binaries in one-dir + [], + exclude_binaries=True, name='{app_name_val}', debug=False, bootloader_ignore_signals=False, strip=False, - upx={config.DEFAULT_SPEC_OPTIONS['use_upx']}, # Use UPX based on config + upx={config.DEFAULT_SPEC_OPTIONS['use_upx']}, runtime_tmpdir=None, - console={not self.is_windowed.get()}, # Set console based on GUI checkbox + console={not self.is_windowed.get()}, disable_windowed_traceback=False, target_arch=None, codesign_identity=None, entitlements_file=None""" - # Add icon option only if a valid relative path was determined if icon_rel_path: exe_str += f",\n icon='{escape_path(icon_rel_path)}'" - exe_str += ")" # Close the EXE parenthesis + exe_str += ")" - # --- COLLECT Section (for one-dir builds) --- collect_str = "" - # Include COLLECT only if 'onefile' checkbox is NOT checked 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']}, # Match UPX setting + upx={config.DEFAULT_SPEC_OPTIONS['use_upx']}, upx_exclude=[], - name='{app_name_val}')""" # Use app name for the output folder + name='{app_name_val}')""" - # --- Combine Spec Content --- spec_header = "# -*- mode: python ; coding: utf-8 -*-\n\nblock_cipher = None\n" - # Add imports potentially needed by generated spec sections (e.g., os for COLLECT) spec_imports = "" - if collect_str: - spec_imports = "import os\n" # COLLECT sometimes needs os + if collect_str: # os may not be needed if a.datas is empty, but safer to include + spec_imports = "import os\n" - # Assemble the final spec content string 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" # Add COLLECT section if applicable + spec_content += f"\n{collect_str}\n" self._log_to_gui(".spec file content generated.", level="DEBUG") - # Return the generated content with a trailing newline return spec_content.strip() + "\n" - def _save_spec_file(self, content): - """ Saves the generated content to the derived .spec file path. """ - # Validate that the derived spec path is set if not self._derived_spec_path: messagebox.showerror("Error", "Cannot save spec: derived path not set or invalid."); return False - # Convert pathlib.Path to string for file operations 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: - # Ensure parent directory exists os.makedirs(os.path.dirname(spec_path_to_save), exist_ok=True) - # Write content to the spec file with open(spec_path_to_save, 'w', encoding='utf-8') as f: f.write(content) - # Log success and update GUI label 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") - return True # Indicate success + self.derived_spec_label_val.config(text=spec_path_to_save, foreground="black") # Update label + return True except Exception as e: - # Log and show error if saving fails 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 - - # --- Build Execution --- def _start_build(self): - """ Validates project, generates target _version.py, generates/saves spec, starts build thread. """ self._log_to_gui("="*20 + " BUILD PROCESS STARTED " + "="*20, level="INFO") - project_dir = self.project_directory_path.get() - # --- Step 1: Validate Project Directory and Derived Paths --- + # --- 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 - # Ensure all necessary derived paths were calculated successfully - if not all([self._derived_main_script_path, self._derived_spec_path, self._derived_source_dir_path]): - messagebox.showerror("Error", "Internal Error: Derived paths not set. Reselect the project directory."); self._log_to_gui("Build cancelled: internal path error.", level="ERROR"); return - - # --- Step 2: Validate Existence of Essential Files/Dirs --- - if not os.path.exists(self._derived_main_script_path): - messagebox.showerror("Error", f"Main script '{self._derived_main_script_path}' not found. Cannot proceed."); self._log_to_gui("Build cancelled: main script missing.", level="ERROR"); return - # Crucially check source directory existence before trying to write _version.py - if not self._derived_source_dir_path.is_dir(): # Check if the Path object points to a directory + # ... (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 - # --- Step 3: Generate _version.py for the Target Project --- - # Define the target path for the _version.py file - target_version_file = self._derived_source_dir_path / "_version.py" - # Call the generation method and check for critical failure - if not self._generate_target_version_file(project_dir, target_version_file): - # Error message already shown by the generation method if writing failed - self._log_to_gui("Build cancelled: Failed to generate target version file.", level="ERROR") - return # Stop the build + # --- RESET STATO BACKUP --- + self._backup_performed = False + self._temp_backup_dir_path = None - # --- Step 4: Generate and Save Spec File (Overwrites existing) --- + # --- 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() - # Check if spec generation was successful if spec_content is None: - self._log_to_gui("Build cancelled: spec generation failed.", level="ERROR"); return - - # Ensure the spec path is a valid string or Path object - if not isinstance(self._derived_spec_path, (str, pathlib.Path)): - messagebox.showerror("Error", "Internal Error: Invalid spec path type."); self._log_to_gui("Build cancelled: internal spec path type error.", level="ERROR"); return - # Convert to string for saving function - spec_path_to_save = str(self._derived_spec_path) - - self._log_to_gui(f"Saving spec file to '{spec_path_to_save}' (will overwrite if exists)...", level="INFO") - # Attempt to save the spec file and stop if it fails + 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"); return - - # --- Step 5: Prepare PyInstaller Command --- + 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 # PyInstaller should run from the project root + # 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']) - # Define output directories relative to the working directory - dist_path_abs = os.path.join(working_dir, config.DEFAULT_SPEC_OPTIONS['output_dir_name']) - work_path_abs = os.path.join(working_dir, config.DEFAULT_SPEC_OPTIONS['work_dir_name']) - # Find the PyInstaller executable pyinstaller_cmd_path = shutil.which("pyinstaller") + # ... (controllo pyinstaller_cmd_path) ... if not pyinstaller_cmd_path: - # Show error if PyInstaller is not found 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() # Reset GUI state + self._on_build_finished(); # _on_build_finished dovrebbe gestire la pulizia del backup return - self._log_to_gui(f"Found PyInstaller executable at: {pyinstaller_cmd_path}", level="DEBUG") + spec_path_to_save_str = str(self._derived_spec_path) - # Construct the PyInstaller command list command = [ pyinstaller_cmd_path, - spec_path_to_save, # Pass the path to the generated/saved spec file - "--distpath", dist_path_abs, # Specify output directory - "--workpath", work_path_abs, # Specify build directory - "--log-level", self.log_level.get() # Set log level from GUI + 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() ] - # Add common flags based on config/defaults - if config.DEFAULT_SPEC_OPTIONS['clean_build']: - command.append("--clean") # Force clean build - if not config.DEFAULT_SPEC_OPTIONS['confirm_overwrite']: - command.append("--noconfirm") # Don't ask for confirmation to overwrite output + + 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") - # --- Step 6: Prepare Environment (Clean Tcl/Tk Vars) --- + 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() - removed_vars = [] - # Remove potentially conflicting Tcl/Tk variables inherited from the GUI environment - if 'TCL_LIBRARY' in build_env: - del build_env['TCL_LIBRARY']; removed_vars.append('TCL_LIBRARY') - if 'TK_LIBRARY' in build_env: - del build_env['TK_LIBRARY']; removed_vars.append('TK_LIBRARY') - # Log if any variables were removed - if removed_vars: - self._log_to_gui(f"Removed environment variables {removed_vars} for external PyInstaller.", level="DEBUG") - else: - self._log_to_gui("No inherited Tcl/Tk environment variables found to remove.", level="DEBUG") - - # Log the final command and working directory + # ... (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: {working_dir}", level="DEBUG") + self._log_to_gui(f"Working Directory: {project_dir}", level="DEBUG") - # --- Step 7: Start Build Thread --- self.build_thread = threading.Thread( target=builder.run_build_in_thread, args=( - command, # The command list - working_dir, # The directory to run in - self.build_queue, # Queue for log messages/status - self._log_to_gui, # Logging function - config.DEFAULT_SPEC_OPTIONS['output_dir_name'], # Expected output folder name - build_env # The cleaned environment dictionary + command, + project_dir, + self.build_queue, + self._log_to_gui, + config.DEFAULT_SPEC_OPTIONS['output_dir_name'], + build_env ), - daemon=True # Allow application to exit even if thread is running + 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 --- \ No newline at end of file