FreeCAD

Форк
0
/
addonmanager_dependency_installer.py 
200 строк · 8.1 Кб
1
# SPDX-License-Identifier: LGPL-2.1-or-later
2
# ***************************************************************************
3
# *                                                                         *
4
# *   Copyright (c) 2022 FreeCAD Project Association                        *
5
# *                                                                         *
6
# *   This file is part of FreeCAD.                                         *
7
# *                                                                         *
8
# *   FreeCAD is free software: you can redistribute it and/or modify it    *
9
# *   under the terms of the GNU Lesser General Public License as           *
10
# *   published by the Free Software Foundation, either version 2.1 of the  *
11
# *   License, or (at your option) any later version.                       *
12
# *                                                                         *
13
# *   FreeCAD is distributed in the hope that it will be useful, but        *
14
# *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
15
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      *
16
# *   Lesser General Public License for more details.                       *
17
# *                                                                         *
18
# *   You should have received a copy of the GNU Lesser General Public      *
19
# *   License along with FreeCAD. If not, see                               *
20
# *   <https://www.gnu.org/licenses/>.                                      *
21
# *                                                                         *
22
# ***************************************************************************
23

24
"""Class to manage installation of sets of Python dependencies."""
25

26
import os
27
import subprocess
28
from typing import List
29

30
from freecad.utils import get_python_exe
31

32
import addonmanager_freecad_interface as fci
33
from addonmanager_pyside_interface import QObject, Signal, is_interruption_requested
34

35
import addonmanager_utilities as utils
36
from addonmanager_installer import AddonInstaller, MacroInstaller
37
from Addon import Addon
38

39
translate = fci.translate
40

41

42
class DependencyInstaller(QObject):
43
    """Install Python dependencies using pip. Intended to be instantiated and then moved into a
44
    QThread: connect the run() function to the QThread's started() signal."""
45

46
    no_python_exe = Signal()
47
    no_pip = Signal(str)  # Attempted command
48
    failure = Signal(str, str)  # Short message, detailed message
49
    finished = Signal()
50

51
    def __init__(
52
        self,
53
        addons: List[Addon],
54
        python_requires: List[str],
55
        python_optional: List[str],
56
        location: os.PathLike = None,
57
    ):
58
        """Install the various types of dependencies that might be specified. If an optional
59
         dependency fails this is non-fatal, but other failures are considered fatal. If location
60
        is specified it overrides the FreeCAD user base directory setting: this is used mostly
61
        for testing purposes and shouldn't be set by normal code in most circumstances.
62
        """
63
        super().__init__()
64
        self.addons = addons
65
        self.python_requires = python_requires
66
        self.python_optional = python_optional
67
        self.location = location
68

69
    def run(self):
70
        """Normally not called directly, but rather connected to the worker thread's started
71
        signal."""
72
        if self._verify_pip():
73
            if self.python_requires or self.python_optional:
74
                if not is_interruption_requested():
75
                    self._install_python_packages()
76
        if not is_interruption_requested():
77
            self._install_addons()
78
        self.finished.emit()
79

80
    def _install_python_packages(self):
81
        """Install required and optional Python dependencies using pip."""
82

83
        if self.location:
84
            vendor_path = os.path.join(self.location, "AdditionalPythonPackages")
85
        else:
86
            vendor_path = utils.get_pip_target_directory()
87
        if not os.path.exists(vendor_path):
88
            os.makedirs(vendor_path)
89

90
        self._install_required(vendor_path)
91
        self._install_optional(vendor_path)
92

93
    def _verify_pip(self) -> bool:
94
        """Ensure that pip is working -- returns True if it is, or False if not. Also emits the
95
        no_pip signal if pip cannot execute."""
96
        python_exe = self._get_python()
97
        if not python_exe:
98
            return False
99
        try:
100
            proc = self._run_pip(["--version"])
101
            fci.Console.PrintMessage(proc.stdout + "\n")
102
        except subprocess.CalledProcessError:
103
            self.no_pip.emit(f"{python_exe} -m pip --version")
104
            return False
105
        return True
106

107
    def _install_required(self, vendor_path: str) -> bool:
108
        """Install the required Python package dependencies. If any fail a failure
109
        signal is emitted and the function exits without proceeding with any additional
110
        installations."""
111
        for pymod in self.python_requires:
112
            if is_interruption_requested():
113
                return False
114
            try:
115
                proc = self._run_pip(
116
                    [
117
                        "install",
118
                        "--disable-pip-version-check",
119
                        "--target",
120
                        vendor_path,
121
                        pymod,
122
                    ]
123
                )
124
                fci.Console.PrintMessage(proc.stdout + "\n")
125
            except subprocess.CalledProcessError as e:
126
                fci.Console.PrintError(str(e) + "\n")
127
                self.failure.emit(
128
                    translate(
129
                        "AddonsInstaller",
130
                        "Installation of Python package {} failed",
131
                    ).format(pymod),
132
                    str(e),
133
                )
134
                return False
135
        return True
136

137
    def _install_optional(self, vendor_path: str):
138
        """Install the optional Python package dependencies. If any fail a message is printed to
139
        the console, but installation of the others continues."""
140
        for pymod in self.python_optional:
141
            if is_interruption_requested():
142
                return
143
            try:
144
                proc = self._run_pip(
145
                    [
146
                        "install",
147
                        "--disable-pip-version-check",
148
                        "--target",
149
                        vendor_path,
150
                        pymod,
151
                    ]
152
                )
153
                fci.Console.PrintMessage(proc.stdout + "\n")
154
            except subprocess.CalledProcessError as e:
155
                fci.Console.PrintError(
156
                    translate("AddonsInstaller", "Installation of optional package failed")
157
                    + ":\n"
158
                    + str(e)
159
                    + "\n"
160
                )
161

162
    def _run_pip(self, args):
163
        python_exe = self._get_python()
164
        final_args = [python_exe, "-m", "pip"]
165
        final_args.extend(args)
166
        return self._subprocess_wrapper(final_args)
167

168
    @staticmethod
169
    def _subprocess_wrapper(args) -> subprocess.CompletedProcess:
170
        """Wrap subprocess call so test code can mock it."""
171
        return utils.run_interruptable_subprocess(args)
172

173
    def _get_python(self) -> str:
174
        """Wrap Python access so test code can mock it."""
175
        python_exe = get_python_exe()
176
        if not python_exe:
177
            self.no_python_exe.emit()
178
        return python_exe
179

180
    def _install_addons(self):
181
        for addon in self.addons:
182
            if is_interruption_requested():
183
                return
184
            fci.Console.PrintMessage(
185
                translate("AddonsInstaller", "Installing required dependency {}").format(addon.name)
186
                + "\n"
187
            )
188
            if addon.macro is None:
189
                installer = AddonInstaller(addon)
190
            else:
191
                installer = MacroInstaller(addon)
192
            result = installer.run()  # Run in this thread, which should be off the GUI thread
193
            if not result:
194
                self.failure.emit(
195
                    translate("AddonsInstaller", "Installation of Addon {} failed").format(
196
                        addon.name
197
                    ),
198
                    "",
199
                )
200
                return
201

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.