OSDN Git Service

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