source: calamares/trunk/fuentes/src/modules/packages/main.py @ 7538

Last change on this file since 7538 was 7538, checked in by kbut, 17 months ago

sync with github

File size: 15.0 KB
Line 
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# === This file is part of Calamares - <https://github.com/calamares> ===
5#
6#   Copyright 2014, Pier Luigi Fiorini <pierluigi.fiorini@gmail.com>
7#   Copyright 2015-2017, Teo Mrnjavac <teo@kde.org>
8#   Copyright 2016-2017, Kyle Robbertze <kyle@aims.ac.za>
9#   Copyright 2017, Alf Gaida <agaida@siduction.org>
10#   Copyright 2018, Adriaan de Groot <groot@kde.org>
11#
12#   Calamares is free software: you can redistribute it and/or modify
13#   it under the terms of the GNU General Public License as published by
14#   the Free Software Foundation, either version 3 of the License, or
15#   (at your option) any later version.
16#
17#   Calamares is distributed in the hope that it will be useful,
18#   but WITHOUT ANY WARRANTY; without even the implied warranty of
19#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20#   GNU General Public License for more details.
21#
22#   You should have received a copy of the GNU General Public License
23#   along with Calamares. If not, see <http://www.gnu.org/licenses/>.
24
25import abc
26from string import Template
27import subprocess
28
29import libcalamares
30from libcalamares.utils import check_target_env_call, target_env_call
31from libcalamares.utils import gettext_path, gettext_languages
32
33import gettext
34_translation = gettext.translation("calamares-python",
35                                   localedir=gettext_path(),
36                                   languages=gettext_languages(),
37                                   fallback=True)
38_ = _translation.gettext
39_n = _translation.ngettext
40
41
42total_packages = 0  # For the entire job
43completed_packages = 0  # Done so far for this job
44group_packages = 0  # One group of packages from an -install or -remove entry
45
46INSTALL = object()
47REMOVE = object()
48mode_packages = None  # Changes to INSTALL or REMOVE
49
50
51def _change_mode(mode):
52    global mode_packages
53    mode_packages = mode
54    libcalamares.job.setprogress(completed_packages * 1.0 / total_packages)
55
56
57def pretty_name():
58    if not group_packages:
59        if (total_packages > 0):
60            # Outside the context of an operation
61            s = _("Processing packages (%(count)d / %(total)d)")
62        else:
63            s = _("Install packages.")
64
65    elif mode_packages is INSTALL:
66        s = _n("Installing one package.",
67               "Installing %(num)d packages.", group_packages)
68    elif mode_packages is REMOVE:
69        s = _n("Removing one package.",
70               "Removing %(num)d packages.", group_packages)
71    else:
72        # No mode, generic description
73        s = _("Install packages.")
74
75    return s % {"num": group_packages,
76                "count": completed_packages,
77                "total": total_packages}
78
79
80class PackageManager(metaclass=abc.ABCMeta):
81    """
82    Package manager base class. A subclass implements package management
83    for a specific backend, and must have a class property `backend`
84    with the string identifier for that backend.
85
86    Subclasses are collected below to populate the list of possible
87    backends.
88    """
89    backend = None
90
91    @abc.abstractmethod
92    def install(self, pkgs, from_local=False):
93        """
94        Install a list of packages (named) into the system.
95        Although this handles lists, in practice it is called
96        with one package at a time.
97
98        @param pkgs: list[str]
99            list of package names
100        @param from_local: bool
101            if True, then these are local packages (on disk) and the
102            pkgs names are paths.
103        """
104        pass
105
106    @abc.abstractmethod
107    def remove(self, pkgs):
108        """
109        Removes packages.
110
111        @param pkgs: list[str]
112            list of package names
113        """
114        pass
115
116    @abc.abstractmethod
117    def update_db(self):
118        pass
119
120    def run(self, script):
121        if script != "":
122            check_target_env_call(script.split(" "))
123
124    def install_package(self, packagedata, from_local=False):
125        """
126        Install a package from a single entry in the install list.
127        This can be either a single package name, or an object
128        with pre- and post-scripts.
129
130        @param packagedata: str|dict
131        @param from_local: bool
132            see install.from_local
133        """
134        if isinstance(packagedata, str):
135            self.install([packagedata], from_local=from_local)
136        else:
137            self.run(packagedata["pre-script"])
138            self.install([packagedata["package"]], from_local=from_local)
139            self.run(packagedata["post-script"])
140
141
142class PMPackageKit(PackageManager):
143    backend = "packagekit"
144
145    def install(self, pkgs, from_local=False):
146        for pkg in pkgs:
147            check_target_env_call(["pkcon", "-py", "install", pkg])
148
149    def remove(self, pkgs):
150        for pkg in pkgs:
151            check_target_env_call(["pkcon", "-py", "remove", pkg])
152
153    def update_db(self):
154        check_target_env_call(["pkcon", "refresh"])
155
156
157class PMZypp(PackageManager):
158    backend = "zypp"
159
160    def install(self, pkgs, from_local=False):
161        check_target_env_call(["zypper", "--non-interactive",
162                               "--quiet-install", "install",
163                               "--auto-agree-with-licenses",
164                               "install"] + pkgs)
165
166    def remove(self, pkgs):
167        check_target_env_call(["zypper", "--non-interactive",
168                               "remove"] + pkgs)
169
170    def update_db(self):
171        check_target_env_call(["zypper", "--non-interactive", "update"])
172
173
174class PMYum(PackageManager):
175    backend = "yum"
176
177    def install(self, pkgs, from_local=False):
178        check_target_env_call(["yum", "install", "-y"] + pkgs)
179
180    def remove(self, pkgs):
181        check_target_env_call(["yum", "--disablerepo=*", "-C", "-y",
182                               "remove"] + pkgs)
183
184    def update_db(self):
185        # Doesn't need updates
186        pass
187
188
189class PMDnf(PackageManager):
190    backend = "dnf"
191
192    def install(self, pkgs, from_local=False):
193        check_target_env_call(["dnf", "install", "-y"] + pkgs)
194
195    def remove(self, pkgs):
196        # ignore the error code for now because dnf thinks removing a
197        # nonexistent package is an error
198        target_env_call(["dnf", "--disablerepo=*", "-C", "-y",
199                         "remove"] + pkgs)
200
201    def update_db(self):
202        # Doesn't need to update explicitly
203        pass
204
205
206class PMUrpmi(PackageManager):
207    backend = "urpmi"
208
209    def install(self, pkgs, from_local=False):
210        check_target_env_call(["urpmi", "--download-all", "--no-suggests",
211                               "--no-verify-rpm", "--fastunsafe",
212                               "--ignoresize", "--nolock",
213                               "--auto"] + pkgs)
214
215    def remove(self, pkgs):
216        check_target_env_call(["urpme", "--auto"] + pkgs)
217
218    def update_db(self):
219        check_target_env_call(["urpmi.update", "-a"])
220
221
222class PMApt(PackageManager):
223    backend = "apt"
224
225    def install(self, pkgs, from_local=False):
226        check_target_env_call(["apt-get", "-q", "-y", "install"] + pkgs)
227
228    def remove(self, pkgs):
229        check_target_env_call(["apt-get", "--purge", "-q", "-y",
230                               "remove"] + pkgs)
231        check_target_env_call(["apt-get", "--purge", "-q", "-y",
232                               "autoremove"])
233
234    def update_db(self):
235        check_target_env_call(["apt-get", "update"])
236
237
238class PMPacman(PackageManager):
239    backend = "pacman"
240
241    def install(self, pkgs, from_local=False):
242        if from_local:
243            pacman_flags = "-U"
244        else:
245            pacman_flags = "-Sy"
246
247        check_target_env_call(["pacman", pacman_flags,
248                               "--noconfirm"] + pkgs)
249
250    def remove(self, pkgs):
251        check_target_env_call(["pacman", "-Rs", "--noconfirm"] + pkgs)
252
253    def update_db(self):
254        check_target_env_call(["pacman", "-Sy"])
255
256
257class PMPortage(PackageManager):
258    backend = "portage"
259
260    def install(self, pkgs, from_local=False):
261        check_target_env_call(["emerge", "-v"] + pkgs)
262
263    def remove(self, pkgs):
264        check_target_env_call(["emerge", "-C"] + pkgs)
265        check_target_env_call(["emerge", "--depclean", "-q"])
266
267    def update_db(self):
268        check_target_env_call(["emerge", "--sync"])
269
270
271class PMEntropy(PackageManager):
272    backend = "entropy"
273
274    def install(self, pkgs, from_local=False):
275        check_target_env_call(["equo", "i"] + pkgs)
276
277    def remove(self, pkgs):
278        check_target_env_call(["equo", "rm"] + pkgs)
279
280    def update_db(self):
281        check_target_env_call(["equo", "update"])
282
283
284class PMDummy(PackageManager):
285    backend = "dummy"
286
287    def install(self, pkgs, from_local=False):
288        libcalamares.utils.debug("Installing " + str(pkgs))
289
290    def remove(self, pkgs):
291        libcalamares.utils.debug("Removing " + str(pkgs))
292
293    def update_db(self):
294        libcalamares.utils.debug("Updating DB")
295
296    def run(self, script):
297        libcalamares.utils.debug("Running script '" + str(script) + "'")
298
299
300class PMPisi(PackageManager):
301    backend = "pisi"
302
303    def install(self, pkgs, from_local=False):
304        check_target_env_call(["pisi", "install" "-y"] + pkgs)
305
306    def remove(self, pkgs):
307        check_target_env_call(["pisi", "remove", "-y"] + pkgs)
308
309    def update_db(self):
310        check_target_env_call(["pisi", "update-repo"])
311
312
313# Collect all the subclasses of PackageManager defined above,
314# and index them based on the backend property of each class.
315backend_managers = [
316    (c.backend, c)
317    for c in globals().values()
318    if type(c) is abc.ABCMeta and issubclass(c, PackageManager) and c.backend]
319
320
321def subst_locale(plist):
322    """
323    Returns a locale-aware list of packages, based on @p plist.
324    Package names that contain LOCALE are localized with the
325    BCP47 name of the chosen system locale; if the system
326    locale is 'en' (e.g. English, US) then these localized
327    packages are dropped from the list.
328
329    @param plist: list[str|dict]
330        Candidate packages to install.
331    @return: list[str|dict]
332    """
333    locale = libcalamares.globalstorage.value("locale")
334    if not locale:
335        # It is possible to skip the locale-setting entirely.
336        # Then pretend it is "en", so that {LOCALE}-decorated
337        # package names are removed from the list.
338        locale = "en"
339
340    ret = []
341    for packagedata in plist:
342        if isinstance(packagedata, str):
343            packagename = packagedata
344        else:
345            packagename = packagedata["package"]
346
347        # Update packagename: substitute LOCALE, and drop packages
348        # if locale is en and LOCALE is in the package name.
349        if locale != "en":
350            packagename = Template(packagename).safe_substitute(LOCALE=locale)
351        elif 'LOCALE' in packagename:
352            packagename = None
353
354        if packagename is not None:
355            # Put it back in packagedata
356            if isinstance(packagedata, str):
357                packagedata = packagename
358            else:
359                packagedata["package"] = packagename
360
361            ret.append(packagedata)
362
363    return ret
364
365
366def run_operations(pkgman, entry):
367    """
368    Call package manager with suitable parameters for the given
369    package actions.
370
371    :param pkgman: PackageManager
372        This is the manager that does the actual work.
373    :param entry: dict
374        Keys are the actions -- e.g. "install" -- to take, and the values
375        are the (list of) packages to apply the action to. The actions are
376        not iterated in a specific order, so it is recommended to use only
377        one action per dictionary. The list of packages may be package
378        names (strings) or package information dictionaries with pre-
379        and post-scripts.
380    """
381    global group_packages, completed_packages, mode_packages
382
383    for key in entry.keys():
384        package_list = subst_locale(entry[key])
385        group_packages = len(package_list)
386        if key == "install":
387            _change_mode(INSTALL)
388            if all([isinstance(x, str) for x in package_list]):
389                pkgman.install(package_list)
390            else:
391                for package in package_list:
392                    pkgman.install_package(package)
393        elif key == "try_install":
394            _change_mode(INSTALL)
395            # we make a separate package manager call for each package so a
396            # single failing package won't stop all of them
397            for package in package_list:
398                try:
399                    pkgman.install_package(package)
400                except subprocess.CalledProcessError:
401                    warn_text = "Could not install package "
402                    warn_text += str(package)
403                    libcalamares.utils.warning(warn_text)
404        elif key == "remove":
405            _change_mode(REMOVE)
406            pkgman.remove(package_list)
407        elif key == "try_remove":
408            _change_mode(REMOVE)
409            for package in package_list:
410                try:
411                    pkgman.remove([package])
412                except subprocess.CalledProcessError:
413                    warn_text = "Could not remove package "
414                    warn_text += package
415                    libcalamares.utils.warning(warn_text)
416        elif key == "localInstall":
417            _change_mode(INSTALL)
418            pkgman.install(package_list, from_local=True)
419
420        completed_packages += len(package_list)
421        libcalamares.job.setprogress(completed_packages * 1.0 / total_packages)
422        libcalamares.utils.debug(pretty_name())
423
424    group_packages = 0
425    _change_mode(None)
426
427
428def run():
429    """
430    Calls routine with detected package manager to install locale packages
431    or remove drivers not needed on the installed system.
432
433    :return:
434    """
435    global mode_packages, total_packages, completed_packages, group_packages
436
437    backend = libcalamares.job.configuration.get("backend")
438
439    for identifier, impl in backend_managers:
440        if identifier == backend:
441            pkgman = impl()
442            break
443    else:
444        return "Bad backend", "backend=\"{}\"".format(backend)
445
446    skip_this = libcalamares.job.configuration.get("skip_if_no_internet", False)
447    if skip_this and not libcalamares.globalstorage.value("hasInternet"):
448        libcalamares.utils.warning( "Package installation has been skipped: no internet" )
449        return None
450
451    update_db = libcalamares.job.configuration.get("update_db", False)
452    if update_db and libcalamares.globalstorage.value("hasInternet"):
453        pkgman.update_db()
454
455    operations = libcalamares.job.configuration.get("operations", [])
456    if libcalamares.globalstorage.contains("packageOperations"):
457        operations += libcalamares.globalstorage.value("packageOperations")
458
459    mode_packages = None
460    total_packages = 0
461    completed_packages = 0
462    for op in operations:
463        for packagelist in op.values():
464            total_packages += len(subst_locale(packagelist))
465
466    if not total_packages:
467        # Avoids potential divide-by-zero in progress reporting
468        return None
469
470    for entry in operations:
471        group_packages = 0
472        libcalamares.utils.debug(pretty_name())
473        run_operations(pkgman, entry)
474
475    mode_packages = None
476
477    libcalamares.job.setprogress(1.0)
478    libcalamares.utils.debug(pretty_name())
479
480    return None
Note: See TracBrowser for help on using the repository browser.