# File: pyinstallerguiwrapper/gui/ast_transformer.py # -*- coding: utf-8 -*- """ AST Transformer for modifying PyInstaller .spec files. """ import ast import sys from typing import Any, Dict, Optional, Callable, Set, List if sys.version_info < (3, 9): try: import astor except ImportError: pass def _default_logger(message, level="DEBUG"): pass class SpecTransformer(ast.NodeTransformer): """ Transforms an existing .spec file AST to reflect GUI options. It aims to modify only the arguments of Analysis, EXE, and COLLECT calls that are managed by the GUI, preserving other parts of the spec file. """ def __init__(self, gui_options: Dict[str, Any], logger_func: Optional[Callable[..., None]] = None): super().__init__() self.gui_options = gui_options self.logger = logger_func if logger_func else _default_logger self.managed_pyinstaller_args: Dict[str, Set[str]] = { 'Analysis': {'scripts', 'pathex', 'datas', 'hiddenimports', 'binaries', 'hookspath', 'runtime_hooks', 'excludes', 'cipher'}, 'EXE': {'name', 'debug', 'bootloader_ignore_signals', 'strip', 'upx', 'runtime_tmpdir', 'console', 'icon', 'disable_windowed_traceback', 'target_arch', 'codesign_identity', 'entitlements_file', 'exclude_binaries'}, 'COLLECT': {'name', 'strip', 'upx', 'upx_exclude'}, 'PYZ': {'cipher'} } self.a_var_name = self.gui_options.get('a_var_name', 'a') self.pyz_var_name = self.gui_options.get('pyz_var_name', 'pyz') self.exe_var_name = self.gui_options.get('exe_var_name', 'exe') def _log(self, message: str, level: str = "DEBUG") -> None: try: self.logger(f"[SpecTransformer] {message}", level=level) except TypeError: self.logger(f"[{level}][SpecTransformer] {message}") @staticmethod def _create_ast_node_static(value_to_convert: Any, logger_for_node_creation: Optional[Callable[..., None]] = None) -> ast.expr: effective_logger = logger_for_node_creation if logger_for_node_creation else _default_logger def _log_node(msg, lvl="DEBUG"): try: effective_logger(f"[ASTNodeCreation] {msg}", level=lvl) except TypeError: effective_logger(f"[{lvl}][ASTNodeCreation] {msg}") if value_to_convert is None: return ast.Constant(value=None) elif isinstance(value_to_convert, (str, int, float, bool, bytes)): return ast.Constant(value=value_to_convert) elif isinstance(value_to_convert, list): return ast.List(elts=[SpecTransformer._create_ast_node_static(v, effective_logger) for v in value_to_convert], ctx=ast.Load()) elif isinstance(value_to_convert, tuple): return ast.Tuple(elts=[SpecTransformer._create_ast_node_static(v, effective_logger) for v in value_to_convert], ctx=ast.Load()) elif isinstance(value_to_convert, (ast.Name, ast.Attribute, ast.expr)): return value_to_convert _log_node(f"Cannot convert Python value '{value_to_convert}' (type: {type(value_to_convert)}) to AST node. Raising TypeError.", "WARNING") raise TypeError(f"Unsupported type for AST conversion: {type(value_to_convert)}") def _create_ast_node(self, value: Any) -> ast.expr: return self._create_ast_node_static(value, self.logger) def visit_Call(self, node: ast.Call) -> ast.Call: self.generic_visit(node) if not isinstance(node.func, ast.Name): return node call_name = node.func.id if call_name not in self.managed_pyinstaller_args: return node self._log(f"Visiting Call to: {call_name}", "DEBUG") existing_keywords = {kw.arg: kw for kw in node.keywords if kw.arg} gui_keywords_to_set = self.managed_pyinstaller_args.get(call_name, set()) # --- CORREZIONE PRINCIPALE: Rimuovi argomenti posizionali se sono gestiti come keyword --- if call_name == 'Analysis': # Rimuovi completamente gli argomenti posizionali per Analysis # e imponi l'uso dei keyword. Questo risolve il TypeError. node.args = [] self._log(" Analysis: Cleared all positional arguments to enforce keyword usage.", "DEBUG") for key_name in gui_keywords_to_set: gui_value: Any = None has_gui_value = False if call_name == 'Analysis': if key_name == 'scripts': gui_value, has_gui_value = self.gui_options.get('analysis_scripts'), True elif key_name == 'pathex': gui_value, has_gui_value = self.gui_options.get('analysis_pathex'), True elif key_name == 'datas': gui_value, has_gui_value = self.gui_options.get('formatted_datas'), True elif key_name == 'hiddenimports': gui_value, has_gui_value = self.gui_options.get('formatted_hiddenimports'), True elif key_name == 'binaries': gui_value, has_gui_value = self.gui_options.get('formatted_binaries'), True elif call_name == 'EXE': if key_name == 'name': gui_value, has_gui_value = self.gui_options.get('app_name'), True elif key_name == 'icon': gui_value, has_gui_value = self.gui_options.get('icon_rel_path'), True elif key_name == 'console': gui_value, has_gui_value = not self.gui_options.get('is_windowed', True), True elif key_name == 'upx': gui_value, has_gui_value = self.gui_options.get('use_upx', True), True elif key_name == 'exclude_binaries': if not self.gui_options.get('is_onefile', False): gui_value, has_gui_value = True, True elif call_name == 'COLLECT': if key_name == 'name': gui_value, has_gui_value = self.gui_options.get('app_name'), True elif key_name == 'upx': gui_value, has_gui_value = self.gui_options.get('use_upx', True), True if has_gui_value: try: new_value_node = self._create_ast_node(gui_value) existing_keywords[key_name] = ast.keyword(arg=key_name, value=new_value_node) self._log(f" {call_name}: Keyword '{key_name}' set/updated from GUI.", "DEBUG") except (TypeError, Exception) as e: self._log(f" {call_name}: Error creating node for keyword '{key_name}': {e}. Skipping.", "WARNING") node.keywords = list(existing_keywords.values()) return node def visit_Module(self, node: ast.Module) -> ast.Module: self.generic_visit(node) gui_cipher_var_name = self.gui_options.get('pyz_cipher_var_name', 'block_cipher') if self._is_cipher_used_in_calls(node, gui_cipher_var_name): has_cipher_assignment = any( isinstance(stmt, ast.Assign) and any(isinstance(t, ast.Name) and t.id == gui_cipher_var_name for t in stmt.targets) for stmt in node.body ) if not has_cipher_assignment: cipher_assignment_node = ast.Assign(targets=[ast.Name(id=gui_cipher_var_name, ctx=ast.Store())], value=self._create_ast_node(None)) node.body.insert(0, cipher_assignment_node) if not self.gui_options.get('is_onefile', False): has_collect_call = any( isinstance(stmt, ast.Assign) and isinstance(stmt.value, ast.Call) and isinstance(stmt.value.func, ast.Name) and stmt.value.func.id == 'COLLECT' for stmt in node.body ) if not has_collect_call: self._log("One-dir mode selected, but no COLLECT call found in spec. Adding it.", "INFO") def name_attr(base_var: str, attr: str) -> ast.Attribute: return ast.Attribute(value=ast.Name(id=base_var, ctx=ast.Load()), attr=attr, ctx=ast.Load()) collect_node = ast.Assign(targets=[ast.Name(id='coll', ctx=ast.Store())], value=ast.Call(func=ast.Name(id='COLLECT', ctx=ast.Load()), args=[ast.Name(id=self.exe_var_name, ctx=ast.Load()), name_attr(self.a_var_name, 'binaries'), name_attr(self.a_var_name, 'zipfiles'), name_attr(self.a_var_name, 'datas')], keywords=[ ast.keyword(arg='strip', value=self._create_ast_node(False)), ast.keyword(arg='upx', value=self._create_ast_node(self.gui_options.get('use_upx', True))), ast.keyword(arg='upx_exclude', value=self._create_ast_node([])), ast.keyword(arg='name', value=self._create_ast_node(self.gui_options.get('app_name'))) ] ) ) node.body.append(collect_node) return node def _is_cipher_used_in_calls(self, module_node: ast.Module, cipher_var_name: str) -> bool: for node in ast.walk(module_node): if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in ['Analysis', 'PYZ']: for kw in node.keywords: if kw.arg == 'cipher' and isinstance(kw.value, ast.Name) and kw.value.id == cipher_var_name: return True return False def visit_Assign(self, node: ast.Assign) -> ast.Assign: gui_cipher_var_name = self.gui_options.get('pyz_cipher_var_name') if gui_cipher_var_name: for target in node.targets: if isinstance(target, ast.Name) and target.id == gui_cipher_var_name: is_already_none = isinstance(node.value, ast.Constant) and node.value.value is None if not is_already_none: self._log(f"Forcing assignment to '{target.id}' to None, as it's managed by the GUI.", "INFO") node.value = self._create_ast_node(None) break if isinstance(node.value, ast.AST): node.value = self.visit(node.value) return node