|
38 | 38 | import json |
39 | 39 | import logging |
40 | 40 | import subprocess |
| 41 | +import platform |
41 | 42 | from pathlib import Path |
42 | 43 | from typing import Optional, Dict, Any, List |
43 | 44 | from datetime import datetime |
@@ -629,21 +630,35 @@ def run_compilation( |
629 | 630 | "duration_ms": 0, |
630 | 631 | } |
631 | 632 |
|
632 | | - # Keep GUI behavior for tool checks when an interactive UI is available. |
633 | | - # In headless mode, some tool-install flows rely on Qt dialogs. |
634 | | - if not self.headless: |
| 633 | + # Ensure required tools are available before real compilation. |
| 634 | + # In headless mode we use a synchronous preflight suited for CI/CD. |
| 635 | + if not (dry_run or self.dry_run): |
635 | 636 | try: |
636 | | - if not engine.ensure_tools_installed(self.gui): |
| 637 | + if self.headless: |
| 638 | + tools_ok, tools_error = self._ensure_engine_tools_headless(engine) |
| 639 | + else: |
| 640 | + tools_ok = bool(engine.ensure_tools_installed(self.gui)) |
| 641 | + tools_error = ( |
| 642 | + "Engine required tools are missing or installation failed" |
| 643 | + ) |
| 644 | + if not tools_ok: |
637 | 645 | return { |
638 | 646 | "success": False, |
639 | | - "error": "Engine required tools are missing or installation failed", |
| 647 | + "error": tools_error, |
640 | 648 | "return_code": -1, |
641 | 649 | "stdout": "", |
642 | 650 | "stderr": "", |
643 | 651 | "duration_ms": 0, |
644 | 652 | } |
645 | | - except Exception: |
646 | | - pass |
| 653 | + except Exception as exc: |
| 654 | + return { |
| 655 | + "success": False, |
| 656 | + "error": f"Engine required tools preflight failed: {exc}", |
| 657 | + "return_code": -1, |
| 658 | + "stdout": "", |
| 659 | + "stderr": "", |
| 660 | + "duration_ms": 0, |
| 661 | + } |
647 | 662 |
|
648 | 663 | # Construire la commande |
649 | 664 | cmd = self.build_command(engine_id, file_path, engine=engine) |
@@ -730,6 +745,207 @@ def run_compilation( |
730 | 745 | "duration_ms": duration_ms, |
731 | 746 | } |
732 | 747 |
|
| 748 | + def _ensure_engine_tools_headless(self, engine) -> tuple[bool, str]: |
| 749 | + """Ensure engine required tools in headless mode with blocking checks/install.""" |
| 750 | + try: |
| 751 | + tools = getattr(engine, "required_tools", {"python": [], "system": []}) |
| 752 | + if not isinstance(tools, dict): |
| 753 | + tools = {"python": [], "system": []} |
| 754 | + python_tools = [str(t).strip() for t in tools.get("python", []) if str(t).strip()] |
| 755 | + system_tools = [str(t).strip() for t in tools.get("system", []) if str(t).strip()] |
| 756 | + |
| 757 | + missing_system = self._missing_system_tools(system_tools) |
| 758 | + if missing_system: |
| 759 | + auto_install = ( |
| 760 | + str(os.environ.get("ARK_AUTO_INSTALL_SYSTEM_TOOLS", "0")).strip().lower() |
| 761 | + in {"1", "true", "yes", "on"} |
| 762 | + ) |
| 763 | + if auto_install: |
| 764 | + try: |
| 765 | + from Core.sys_deps import install_system_packages |
| 766 | + |
| 767 | + if not install_system_packages(missing_system, gui=None): |
| 768 | + return ( |
| 769 | + False, |
| 770 | + "Missing system tools and automatic installation failed: " |
| 771 | + + ", ".join(missing_system), |
| 772 | + ) |
| 773 | + except Exception as exc: |
| 774 | + return ( |
| 775 | + False, |
| 776 | + f"Missing system tools ({', '.join(missing_system)}) and install failed: {exc}", |
| 777 | + ) |
| 778 | + else: |
| 779 | + return ( |
| 780 | + False, |
| 781 | + "Missing system tools: " |
| 782 | + + ", ".join(missing_system) |
| 783 | + + ". Set ARK_AUTO_INSTALL_SYSTEM_TOOLS=1 to allow auto-install.", |
| 784 | + ) |
| 785 | + |
| 786 | + if not python_tools: |
| 787 | + return True, "" |
| 788 | + |
| 789 | + manager = getattr(self.gui, "venv_manager", None) |
| 790 | + use_system = bool(getattr(self.gui, "use_system_python", False)) |
| 791 | + venv_path = None |
| 792 | + if manager is not None and not use_system: |
| 793 | + try: |
| 794 | + venv_path = manager.resolve_project_venv() |
| 795 | + except Exception: |
| 796 | + venv_path = None |
| 797 | + if not venv_path: |
| 798 | + return False, "No resolved project venv for Python tool installation" |
| 799 | + |
| 800 | + missing_python = self._missing_python_tools( |
| 801 | + python_tools, |
| 802 | + manager=manager, |
| 803 | + use_system=use_system, |
| 804 | + venv_path=venv_path, |
| 805 | + ) |
| 806 | + if not missing_python: |
| 807 | + return True, "" |
| 808 | + |
| 809 | + ok_install, install_error = self._install_python_tools_headless( |
| 810 | + missing_python, |
| 811 | + manager=manager, |
| 812 | + use_system=use_system, |
| 813 | + venv_path=venv_path, |
| 814 | + ) |
| 815 | + if not ok_install: |
| 816 | + return False, install_error |
| 817 | + |
| 818 | + still_missing = self._missing_python_tools( |
| 819 | + python_tools, |
| 820 | + manager=manager, |
| 821 | + use_system=use_system, |
| 822 | + venv_path=venv_path, |
| 823 | + ) |
| 824 | + if still_missing: |
| 825 | + return ( |
| 826 | + False, |
| 827 | + "Python tools still missing after installation: " |
| 828 | + + ", ".join(still_missing), |
| 829 | + ) |
| 830 | + return True, "" |
| 831 | + except Exception as exc: |
| 832 | + return False, f"Headless tools preflight failed: {exc}" |
| 833 | + |
| 834 | + def _missing_system_tools(self, tools: list[str]) -> list[str]: |
| 835 | + """Return missing system tools by checking PATH availability.""" |
| 836 | + if not tools: |
| 837 | + return [] |
| 838 | + try: |
| 839 | + from Core.sys_deps import check_system_packages |
| 840 | + |
| 841 | + return [tool for tool in tools if not check_system_packages([tool])] |
| 842 | + except Exception: |
| 843 | + return tools |
| 844 | + |
| 845 | + def _missing_python_tools( |
| 846 | + self, |
| 847 | + tools: list[str], |
| 848 | + *, |
| 849 | + manager, |
| 850 | + use_system: bool, |
| 851 | + venv_path: str | None, |
| 852 | + ) -> list[str]: |
| 853 | + """Return missing python tools according to current interpreter mode.""" |
| 854 | + missing: list[str] = [] |
| 855 | + for tool in tools: |
| 856 | + installed = False |
| 857 | + try: |
| 858 | + if manager is not None: |
| 859 | + if use_system: |
| 860 | + installed = bool(manager.is_tool_installed_system(tool)) |
| 861 | + elif venv_path: |
| 862 | + installed = bool(manager.is_tool_installed(venv_path, tool)) |
| 863 | + except Exception: |
| 864 | + installed = False |
| 865 | + if not installed: |
| 866 | + missing.append(tool) |
| 867 | + return missing |
| 868 | + |
| 869 | + def _tool_install_candidates(self, tool: str) -> list[str]: |
| 870 | + """Build pip install candidates from a generic tool identifier.""" |
| 871 | + t = str(tool or "").strip() |
| 872 | + if not t: |
| 873 | + return [] |
| 874 | + low = t.lower() |
| 875 | + variants = [low] |
| 876 | + if "_" in low: |
| 877 | + variants.append(low.replace("_", "-")) |
| 878 | + variants.append(low.replace("_", "")) |
| 879 | + if "-" in low: |
| 880 | + variants.append(low.replace("-", "_")) |
| 881 | + variants.append(low.replace("-", "")) |
| 882 | + |
| 883 | + unique: list[str] = [] |
| 884 | + seen: set[str] = set() |
| 885 | + for item in variants: |
| 886 | + key = item.lower() |
| 887 | + if not item or key in seen: |
| 888 | + continue |
| 889 | + seen.add(key) |
| 890 | + unique.append(item) |
| 891 | + return unique |
| 892 | + |
| 893 | + def _install_python_tools_headless( |
| 894 | + self, |
| 895 | + tools: list[str], |
| 896 | + *, |
| 897 | + manager, |
| 898 | + use_system: bool, |
| 899 | + venv_path: str | None, |
| 900 | + ) -> tuple[bool, str]: |
| 901 | + """Install python tools synchronously for CI/CD headless runs.""" |
| 902 | + if not tools: |
| 903 | + return True, "" |
| 904 | + |
| 905 | + python_bin = sys.executable |
| 906 | + if manager is not None and not use_system: |
| 907 | + if not venv_path: |
| 908 | + return False, "No venv path available for Python tool installation" |
| 909 | + try: |
| 910 | + python_bin = manager.python_path(venv_path) |
| 911 | + except Exception: |
| 912 | + python_bin = sys.executable |
| 913 | + |
| 914 | + extra_args: list[str] = [] |
| 915 | + if use_system and platform.system() == "Linux": |
| 916 | + extra_args.append("--break-system-packages") |
| 917 | + |
| 918 | + for tool in tools: |
| 919 | + installed = False |
| 920 | + last_error = "" |
| 921 | + for candidate in self._tool_install_candidates(tool): |
| 922 | + cmd = [python_bin, "-m", "pip", "install"] + extra_args + [candidate] |
| 923 | + try: |
| 924 | + proc = subprocess.run( |
| 925 | + cmd, |
| 926 | + stdout=subprocess.PIPE, |
| 927 | + stderr=subprocess.PIPE, |
| 928 | + text=True, |
| 929 | + timeout=1800, |
| 930 | + check=False, |
| 931 | + ) |
| 932 | + except Exception as exc: |
| 933 | + last_error = str(exc) |
| 934 | + continue |
| 935 | + |
| 936 | + if proc.returncode != 0: |
| 937 | + err = (proc.stderr or proc.stdout or "").strip() |
| 938 | + last_error = err or f"pip install failed for {candidate}" |
| 939 | + continue |
| 940 | + |
| 941 | + installed = True |
| 942 | + break |
| 943 | + |
| 944 | + if not installed: |
| 945 | + return False, f"Unable to install Python tool '{tool}': {last_error}" |
| 946 | + |
| 947 | + return True, "" |
| 948 | + |
733 | 949 | def execute(self) -> Dict[str, Any]: |
734 | 950 | """ |
735 | 951 | Exécute l'application avec les paramètres configurés. |
@@ -873,13 +1089,13 @@ def main(): |
873 | 1089 | python -m Core.engines_loader.engines_only_mod --list-engines |
874 | 1090 | |
875 | 1091 | # Check engine compatibility |
876 | | - python -m Core.engines_loader.engines_only_mod --check-compat nuitka |
| 1092 | + python -m Core.engines_loader.engines_only_mod --check-compat <engine_id> |
877 | 1093 | |
878 | 1094 | # Compile a file with a specific engine (dry-run) |
879 | | - python -m Core.engines_loader.engines_only_mod --engine nuitka --dry-run script.py |
| 1095 | + python -m Core.engines_loader.engines_only_mod --engine <engine_id> --dry-run script.py |
880 | 1096 | |
881 | 1097 | # Compile a file with a specific engine |
882 | | - python -m Core.engines_loader.engines_only_mod --engine pyinstaller --file script.py |
| 1098 | + python -m Core.engines_loader.engines_only_mod --engine <engine_id> --file script.py |
883 | 1099 | """, |
884 | 1100 | ) |
885 | 1101 |
|
|
0 commit comments