2 # -*- coding: utf-8 -*-
4 # === This file is part of Calamares - <https://github.com/calamares> ===
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>
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.
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.
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/>.
27 from string import Template
31 from libcalamares.utils import check_target_env_call, target_env_call
32 from libcalamares.utils import gettext_path, gettext_languages
35 _translation = gettext.translation("calamares-python",
36 localedir=gettext_path(),
37 languages=gettext_languages(),
39 _ = _translation.gettext
40 _n = _translation.ngettext
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
49 mode_packages = None # Changes to INSTALL or REMOVE
52 def _change_mode(mode):
55 libcalamares.job.setprogress(completed_packages * 1.0 / total_packages)
59 return _("Install packages.")
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)")
68 s = _("Install packages.")
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)
77 # No mode, generic description
78 s = _("Install packages.")
80 return s % {"num": group_packages,
81 "count": completed_packages,
82 "total": total_packages}
85 class PackageManager(metaclass=abc.ABCMeta):
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.
91 Subclasses are collected below to populate the list of possible
97 def install(self, pkgs, from_local=False):
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.
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.
112 def remove(self, pkgs):
116 @param pkgs: list[str]
117 list of package names
125 def run(self, script):
127 check_target_env_call(script.split(" "))
129 def install_package(self, packagedata, from_local=False):
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.
136 @param packagedata: str|dict
137 @param from_local: bool
138 see install.from_local
140 if isinstance(packagedata, str):
141 self.install([packagedata], from_local=from_local)
143 self.run(packagedata["pre-script"])
144 self.install([packagedata["package"]], from_local=from_local)
145 self.run(packagedata["post-script"])
147 def remove_package(self, packagedata):
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.
154 @param packagedata: str|dict
156 if isinstance(packagedata, str):
157 self.remove([packagedata])
159 self.run(packagedata["pre-script"])
160 self.remove([packagedata["package"]])
161 self.run(packagedata["post-script"])
164 class PMPackageKit(PackageManager):
165 backend = "packagekit"
167 def install(self, pkgs, from_local=False):
169 check_target_env_call(["pkcon", "-py", "install", pkg])
171 def remove(self, pkgs):
173 check_target_env_call(["pkcon", "-py", "remove", pkg])
176 check_target_env_call(["pkcon", "refresh"])
178 def update_system(self):
179 check_target_env_call(["pkcon", "-py", "update"])
181 class PMZypp(PackageManager):
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",
190 def remove(self, pkgs):
191 check_target_env_call(["zypper", "--non-interactive",
195 check_target_env_call(["zypper", "--non-interactive", "update"])
197 def update_system(self):
198 # Doesn't need to update the system explicitly
201 class PMYum(PackageManager):
204 def install(self, pkgs, from_local=False):
205 check_target_env_call(["yum", "-y", "install"] + pkgs)
207 def remove(self, pkgs):
208 check_target_env_call(["yum", "--disablerepo=*", "-C", "-y",
212 # Doesn't need updates
215 def update_system(self):
216 check_target_env_call(["yum", "-y", "upgrade"])
218 class PMDnf(PackageManager):
221 def install(self, pkgs, from_local=False):
222 check_target_env_call(["dnf", "-y", "install"] + pkgs)
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",
231 # Doesn't need updates
234 def update_system(self):
235 check_target_env_call(["dnf", "-y", "upgrade"])
238 class PMUrpmi(PackageManager):
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",
247 def remove(self, pkgs):
248 check_target_env_call(["urpme", "--auto"] + pkgs)
251 check_target_env_call(["urpmi.update", "-a"])
253 def update_system(self):
254 # Doesn't need to update the system explicitly
258 class PMApt(PackageManager):
261 def install(self, pkgs, from_local=False):
262 check_target_env_call(["apt-get", "-q", "-y", "install"] + pkgs)
264 def remove(self, pkgs):
265 check_target_env_call(["apt-get", "--purge", "-q", "-y",
267 check_target_env_call(["apt-get", "--purge", "-q", "-y",
271 check_target_env_call(["apt-get", "update"])
273 def update_system(self):
274 # Doesn't need to update the system explicitly
278 class PMPacman(PackageManager):
281 def install(self, pkgs, from_local=False):
287 check_target_env_call(["pacman", pacman_flags,
288 "--noconfirm"] + pkgs)
290 def remove(self, pkgs):
291 check_target_env_call(["pacman", "-Rsnc", "--noconfirm"] + pkgs)
294 check_target_env_call(["pacman", "-Syy"])
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"])
303 class PMPortage(PackageManager):
306 def install(self, pkgs, from_local=False):
307 check_target_env_call(["emerge", "-v"] + pkgs)
309 def remove(self, pkgs):
310 check_target_env_call(["emerge", "-C"] + pkgs)
311 check_target_env_call(["emerge", "--depclean", "-q"])
314 check_target_env_call(["emerge", "--sync"])
316 def update_system(self):
317 # Doesn't need to update the system explicitly
321 class PMEntropy(PackageManager):
324 def install(self, pkgs, from_local=False):
325 check_target_env_call(["equo", "i"] + pkgs)
327 def remove(self, pkgs):
328 check_target_env_call(["equo", "rm"] + pkgs)
331 check_target_env_call(["equo", "update"])
333 def update_system(self):
334 # Doesn't need to update the system explicitly
338 class PMDummy(PackageManager):
341 def install(self, pkgs, from_local=False):
342 from time import sleep
343 libcalamares.utils.debug("Dummy backend: Installing " + str(pkgs))
346 def remove(self, pkgs):
347 from time import sleep
348 libcalamares.utils.debug("Dummy backend: Removing " + str(pkgs))
352 libcalamares.utils.debug("Dummy backend: Updating DB")
354 def update_system(self):
355 libcalamares.utils.debug("Dummy backend: Updating System")
357 def run(self, script):
358 libcalamares.utils.debug("Dummy backend: Running script '" + str(script) + "'")
361 class PMPisi(PackageManager):
364 def install(self, pkgs, from_local=False):
365 check_target_env_call(["pisi", "install" "-y"] + pkgs)
367 def remove(self, pkgs):
368 check_target_env_call(["pisi", "remove", "-y"] + pkgs)
371 check_target_env_call(["pisi", "update-repo"])
373 def update_system(self):
374 # Doesn't need to update the system explicitly
378 class PMApk(PackageManager):
381 def install(self, pkgs, from_local=False):
383 check_target_env_call(["apk", "add", pkg])
385 def remove(self, pkgs):
387 check_target_env_call(["apk", "del", pkg])
390 check_target_env_call(["apk", "update"])
392 def update_system(self):
393 check_target_env_call(["apk", "upgrade", "--available"])
396 # Collect all the subclasses of PackageManager defined above,
397 # and index them based on the backend property of each class.
400 for c in globals().values()
401 if type(c) is abc.ABCMeta and issubclass(c, PackageManager) and c.backend]
404 def subst_locale(plist):
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.
412 @param plist: list[str|dict]
413 Candidate packages to install.
414 @return: list[str|dict]
416 locale = libcalamares.globalstorage.value("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.
424 for packagedata in plist:
425 if isinstance(packagedata, str):
426 packagename = packagedata
428 packagename = packagedata["package"]
430 # Update packagename: substitute LOCALE, and drop packages
431 # if locale is en and LOCALE is in the package name.
433 packagename = Template(packagename).safe_substitute(LOCALE=locale)
434 elif 'LOCALE' in packagename:
437 if packagename is not None:
438 # Put it back in packagedata
439 if isinstance(packagedata, str):
440 packagedata = packagename
442 packagedata["package"] = packagename
444 ret.append(packagedata)
449 def run_operations(pkgman, entry):
451 Call package manager with suitable parameters for the given
454 :param pkgman: PackageManager
455 This is the manager that does the actual work.
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-
464 global group_packages, completed_packages, mode_packages
466 for key in entry.keys():
467 package_list = subst_locale(entry[key])
468 group_packages = len(package_list)
470 _change_mode(INSTALL)
471 if all([isinstance(x, str) for x in package_list]):
472 pkgman.install(package_list)
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:
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":
489 if all([isinstance(x, str) for x in package_list]):
490 pkgman.remove(package_list)
492 for package in package_list:
493 pkgman.remove_package(package)
494 elif key == "try_remove":
496 for package in package_list:
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)
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]))
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()))
524 Calls routine with detected package manager to install locale packages
525 or remove drivers not needed on the installed system.
529 global mode_packages, total_packages, completed_packages, group_packages
531 backend = libcalamares.job.configuration.get("backend")
533 for identifier, impl in backend_managers:
534 if identifier == backend:
538 return "Bad backend", "backend=\"{}\"".format(backend)
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" )
545 update_db = libcalamares.job.configuration.get("update_db", False)
546 if update_db and libcalamares.globalstorage.value("hasInternet"):
549 update_system = libcalamares.job.configuration.get("update_system", False)
550 if update_system and libcalamares.globalstorage.value("hasInternet"):
551 pkgman.update_system()
553 operations = libcalamares.job.configuration.get("operations", [])
554 if libcalamares.globalstorage.contains("packageOperations"):
555 operations += libcalamares.globalstorage.value("packageOperations")
559 completed_packages = 0
560 for op in operations:
561 for packagelist in op.values():
562 total_packages += len(subst_locale(packagelist))
564 if not total_packages:
565 # Avoids potential divide-by-zero in progress reporting
568 for entry in operations:
570 libcalamares.utils.debug(pretty_name())
571 run_operations(pkgman, entry)
575 libcalamares.job.setprogress(1.0)
576 libcalamares.utils.debug(pretty_name())