2 # -*- coding: utf-8 -*-
6 # Copyright © 2013-2018 Antergos
8 # This file is part of Cnchi.
10 # Cnchi is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # Cnchi is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with Cnchi; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 """ Package list generation module. """
31 from requests.exceptions import RequestException
33 import xml.etree.cElementTree as elementTree
37 import pacman.pac as pac
39 from misc.events import Events
40 import misc.extra as misc
41 from misc.extra import InstallError
43 import hardware.hardware as hardware
45 from lembrame.lembrame import Lembrame
47 # When testing, no _() is available
50 except NameError as err:
55 class SelectPackages():
56 """ Package list creation class """
58 PKGLIST_URL = 'https://gitlab.com/reborn-os-team/cnchi/blob/master/Cnchi/packages.xml'
60 def __init__(self, settings, callback_queue):
61 """ Initialize package class """
63 self.events = Events(callback_queue)
64 self.settings = settings
65 self.desktop = self.settings.get('desktop')
67 # Packages to be removed
70 # Packages to be installed
77 # If Lembrame enabled set pacman.conf pointing to the decrypted folder
78 if self.settings.get('feature_lembrame'):
79 self.lembrame = Lembrame(self.settings)
80 path = os.path.join(self.lembrame.config.folder_file_path, 'pacman.conf')
81 self.settings.set('pacman_config_file', path)
83 def create_package_list(self):
84 """ Create package list """
89 logging.debug("Refreshing pacman databases...")
90 self.refresh_pacman_databases()
91 logging.debug("Pacman ready")
93 logging.debug("Selecting packages...")
94 self.select_packages()
95 logging.debug("Packages selected")
97 # Fix bug #263 (v86d moved from [extra] to AUR)
98 if "v86d" in self.packages:
99 self.packages.remove("v86d")
100 logging.debug("Removed 'v86d' package from list")
103 self.settings.set('is_vbox', True)
105 @misc.raise_privileges
106 def refresh_pacman_databases(self):
107 """ Updates pacman databases """
110 pacman = pac.Pac(self.settings.get('pacman_config_file'), self.events.queue)
111 except Exception as ex:
113 "Can't initialize pyalpm. An exception of type {0} occured. Arguments:\n{1!r}")
114 message = template.format(type(ex).__name__, ex.args)
115 logging.error(message)
116 raise InstallError(message)
118 # Refresh pacman databases
119 if not pacman.refresh():
120 logging.error("Can't refresh pacman databases.")
121 txt = _("Can't refresh pacman databases.")
122 raise InstallError(txt)
127 except Exception as ex:
129 "Can't release pyalpm. An exception of type {0} occured. Arguments:\n{1!r}")
130 message = template.format(type(ex).__name__, ex.args)
131 logging.error(message)
132 raise InstallError(message)
134 def add_package(self, pkg):
135 """ Adds xml node text to our package list
136 returns TRUE if the package is added """
137 libs = desktop_info.LIBS
139 arch = pkg.attrib.get('arch')
140 if arch and arch != os.uname()[-1]:
143 lang = pkg.attrib.get('lang')
144 locale = self.settings.get("locale").split('.')[0][:2]
145 if lang and lang != locale:
148 lib = pkg.attrib.get('lib')
149 if lib and self.desktop not in libs[lib]:
152 desktops = pkg.attrib.get('desktops')
153 if desktops and self.desktop not in desktops:
156 # If package is a Desktop Manager or a Network Manager,
157 # save the name to activate the correct service later
158 if pkg.attrib.get('dm'):
159 self.settings.set("desktop_manager", pkg.attrib.get('name'))
160 if pkg.attrib.get('nm'):
161 self.settings.set("network_manager", pkg.attrib.get('name'))
163 # check conflicts attrib
164 conflicts = pkg.attrib.get('conflicts')
166 self.add_conflicts(pkg.attrib.get('conflicts'))
168 # finally, add package
169 self.packages.append(pkg.text)
172 def load_xml_local(self, xml_filename):
173 """ Load xml packages list from file name """
174 self.events.add('info', _("Reading local package list..."))
175 if os.path.exists(xml_filename):
176 logging.debug("Loading %s", xml_filename)
177 xml_tree = elementTree.parse(xml_filename)
178 self.xml_root = xml_tree.getroot()
180 logging.warning("Cannot find %s file", xml_filename)
182 def load_xml_remote(self):
183 """ Load xml packages list from url """
184 self.events.add('info', _("Getting online package list..."))
185 url = SelectPackages.PKGLIST_URL
186 logging.debug("Getting url %s...", url)
188 req = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'})
189 self.xml_root = elementTree.fromstring(req.content)
190 except RequestException as url_error:
191 msg = "Can't retrieve remote package list: {}".format(
195 def load_xml_root_node(self):
196 """ Loads xml data, storing the root node """
199 alternate_package_list = self.settings.get('alternate_package_list')
200 if alternate_package_list:
201 # Use file passed by parameter (overrides server one)
202 self.load_xml_local(alternate_package_list)
204 if self.xml_root is None:
205 # The list of packages is retrieved from an online XML to let us
206 # control the pkgname in case of any modification
207 self.load_xml_remote()
209 if self.xml_root is None:
210 # If the installer can't retrieve the remote file Cnchi will use
211 # a local copy, which might be updated or not.
212 xml_filename = os.path.join(self.settings.get('data'), 'packages.xml')
213 self.load_xml_local(xml_filename)
215 if self.xml_root is None:
216 txt = "Could not load packages XML file (neither local nor from the Internet)"
218 txt = _("Could not load packages XML file (neither local nor from the Internet)")
219 raise InstallError(txt)
221 def add_drivers(self):
222 """ Add package drivers """
224 # Detect which hardware drivers are needed
225 hardware_install = hardware.HardwareInstall(
226 self.settings.get('cnchi'),
227 self.settings.get('feature_graphic_drivers'))
228 driver_names = hardware_install.get_found_driver_names()
231 "Hardware module detected these drivers: %s",
234 # Add needed hardware packages to our list
235 hardware_pkgs = hardware_install.get_packages()
238 "Hardware module added these packages: %s",
239 ", ".join(hardware_pkgs))
240 if 'virtualbox' in hardware_pkgs:
242 self.packages.extend(hardware_pkgs)
244 # Add conflicting hardware packages to our conflicts list
245 self.conflicts.extend(hardware_install.get_conflicts())
246 except Exception as ex:
248 "Error in hardware module. An exception of type {0} occured. Arguments:\n{1!r}")
249 message = template.format(type(ex).__name__, ex.args)
250 logging.error(message)
252 def add_filesystems(self):
253 """ Add filesystem packages """
254 logging.debug("Adding filesystem packages")
255 for child in self.xml_root.iter("filesystems"):
256 for pkg in child.iter('pkgname'):
257 self.add_package(pkg)
260 if self.settings.get('zfs'):
261 logging.debug("Adding zfs packages")
262 for child in self.xml_root.iter("zfs"):
263 for pkg in child.iter('pkgname'):
264 self.add_package(pkg)
266 def maybe_add_chinese_fonts(self):
267 """ Add chinese fonts if necessary """
268 lang_code = self.settings.get("language_code")
269 if lang_code in ["zh_TW", "zh_CN"]:
270 logging.debug("Selecting chinese fonts.")
271 for child in self.xml_root.iter('chinese'):
272 for pkg in child.iter('pkgname'):
273 self.add_package(pkg)
275 def maybe_add_bootloader(self):
276 """ Add bootloader packages if needed """
277 if self.settings.get('bootloader_install'):
278 boot_loader = self.settings.get('bootloader')
279 bootloader_found = False
280 for child in self.xml_root.iter('bootloader'):
281 if child.attrib.get('name') == boot_loader:
282 txt = _("Adding '%s' bootloader packages")
283 logging.debug(txt, boot_loader)
284 bootloader_found = True
285 for pkg in child.iter('pkgname'):
286 self.add_package(pkg)
287 if not bootloader_found:
289 "Couldn't find %s bootloader packages!", boot_loader)
291 def add_edition_packages(self):
292 """ Add common and specific edition packages """
293 for editions in self.xml_root.iter('editions'):
294 for edition in editions.iter('edition'):
295 name = edition.attrib.get('name').lower()
297 # Add common packages to all desktops (including base)
299 for pkg in edition.iter('pkgname'):
300 self.add_package(pkg)
302 # Add common graphical packages (not if installing 'base')
303 if name == 'graphic' and self.desktop != 'base':
304 for pkg in edition.iter('pkgname'):
305 self.add_package(pkg)
307 # Add specific desktop packages
308 if name == self.desktop:
309 logging.debug("Adding %s desktop packages", self.desktop)
310 for pkg in edition.iter('pkgname'):
311 self.add_package(pkg)
313 def maybe_add_vbox_packages(self):
314 """ Adds specific virtualbox packages if running inside a VM """
316 # Add virtualbox-guest-utils-nox package if 'base' is installed in a vbox vm
317 if self.desktop == 'base':
318 self.packages.append('virtualbox-guest-utils-nox')
320 # Add linux-lts-headers if LTS kernel is installed in a vbox vm
321 if self.settings.get('feature_lts'):
322 self.packages.append('linux-lts-headers')
324 def select_packages(self):
325 """ Get package list from the Internet and add specific packages to it """
329 self.load_xml_root_node()
331 # Add common and desktop specific packages
332 self.add_edition_packages()
334 # Add drivers' packages
337 # Add file system packages
338 self.add_filesystems()
340 # Add chinese fonts (if necessary)
341 self.maybe_add_chinese_fonts()
343 # Add bootloader (if user chose it)
344 self.maybe_add_bootloader()
346 # Add extra virtualbox packages (if needed)
347 self.maybe_add_vbox_packages()
349 # Check for user desired features and add them to our installation
351 "Check for user desired features and add them to our installation")
353 logging.debug("All features needed packages have been added")
355 # Add Lembrame packages but install Cnchi defaults too
356 # TODO: Lembrame has to generate a better package list indicating DM and stuff
357 if self.settings.get("feature_lembrame"):
358 self.events.add('info', _("Appending list of packages from Lembrame"))
359 self.packages = self.packages + self.lembrame.get_pacman_packages()
361 # Remove duplicates and conflicting packages
362 self.cleanup_packages_list()
363 logging.debug("Packages list: %s", ','.join(self.packages))
365 # Check if all packages ARE in the repositories
366 # This is done mainly to avoid errors when Arch removes a package silently
367 self.check_packages()
369 def check_packages(self):
370 """ Checks that all selected packages ARE in the repositories """
372 self.events.add('percent', 0)
373 self.events.add('info', _("Checking that all selected packages are available online..."))
374 num_pkgs = len(self.packages)
375 for index, pkg_name in enumerate(self.packages):
376 # TODO: Use libalpm instead
377 cmd = ["/usr/bin/pacman", "-Ss", pkg_name]
379 output = subprocess.check_output(cmd).decode()
380 except subprocess.CalledProcessError:
383 if pkg_name not in output:
384 not_found.append(pkg_name)
385 logging.error("Package %s...NOT FOUND!", pkg_name)
387 percent = (index + 1) / num_pkgs
388 self.events.add('percent', percent)
391 txt = _("Cannot find these packages: {}").format(', '.join(not_found))
392 raise misc.InstallError(txt)
395 def cleanup_packages_list(self):
396 """ Cleans up a bit our packages list """
398 self.packages = list(set(self.packages))
399 self.conflicts = list(set(self.conflicts))
401 # Check the list of packages for empty strings and remove any that we find.
402 self.packages = [pkg for pkg in self.packages if pkg != '']
403 self.conflicts = [pkg for pkg in self.conflicts if pkg != '']
405 # Remove any package from self.packages that is already in self.conflicts
407 logging.debug("Conflicts list: %s", ", ".join(self.conflicts))
408 for pkg in self.conflicts:
409 if pkg in self.packages:
410 self.packages.remove(pkg)
412 def add_conflicts(self, conflicts):
413 """ Maintains a list of conflicting packages """
416 for conflict in conflicts.split(','):
417 conflict = conflict.rstrip()
418 if conflict not in self.conflicts:
419 self.conflicts.append(conflict)
421 self.conflicts.append(conflicts)
423 def add_hunspell(self, language_code):
424 """ Adds hunspell dictionary """
425 # Try to read available codes from hunspell.txt
426 data_dir = self.settings.get("data")
427 path = os.path.join(data_dir, "hunspell.txt")
428 if os.path.exists(path):
429 with open(path, 'r') as lang_file:
430 lang_codes = lang_file.read().split()
432 # hunspell.txt not available, let's use this hardcoded version (as failsafe)
434 'de-frami', 'de', 'en', 'en_AU', 'en_CA', 'en_GB', 'en_US',
435 'es_any', 'es_ar', 'es_bo', 'es_cl', 'es_co', 'es_cr', 'es_cu',
436 'es_do', 'es_ec', 'es_es', 'es_gt', 'es_hn', 'es_mx', 'es_ni',
437 'es_pa', 'es_pe', 'es_pr', 'es_py', 'es_sv', 'es_uy', 'es_ve',
438 'fr', 'he', 'it', 'ro', 'el', 'hu', 'nl', 'pl']
440 if language_code in lang_codes:
441 pkg_text = "hunspell-{0}".format(language_code)
443 "Adding hunspell dictionary for %s language", pkg_text)
444 self.packages.append(pkg_text)
447 "No hunspell language dictionary found for %s language code", language_code)
449 def add_libreoffice_language(self):
450 """ Adds libreoffice language package """
451 lang_name = self.settings.get('language_name').lower()
452 if lang_name == 'english':
453 # There're some English variants available but not all of them.
454 locale = self.settings.get('locale').split('.')[0]
455 if locale in ['en_GB', 'en_ZA']:
460 # All the other language packs use their language code
461 code = self.settings.get('language_code')
464 code = code.replace('_', '-').lower()
465 pkg_text = "libreoffice-fresh-{0}".format(code)
467 "Adding libreoffice language package (%s)", pkg_text)
468 self.packages.append(pkg_text)
469 self.add_hunspell(code)
471 def add_firefox_language(self):
472 """ Add firefox language package """
473 # Try to load available languages from firefox.txt (easy updating if necessary)
474 data_dir = self.settings.get("data")
475 path = os.path.join(data_dir, "firefox.txt")
476 if os.path.exists(path):
477 with open(path, 'r') as lang_file:
478 lang_codes = lang_file.read().split()
480 # Couldn't find firefox.txt, use this hardcoded version then (as failsafe)
482 'ach', 'af', 'an', 'ar', 'as', 'ast', 'az', 'be', 'bg', 'bn-bd',
483 'bn-in', 'br', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'dsb', 'el',
484 'en-gb', 'en-us', 'en-za', 'eo', 'es-ar', 'es-cl', 'es-es',
485 'es-mx', 'et', 'eu', 'fa', 'ff', 'fi', 'fr', 'fy-nl', 'ga-ie',
486 'gd', 'gl', 'gu-in', 'he', 'hi-in', 'hr', 'hsb', 'hu', 'hy-am',
487 'id', 'is', 'it', 'ja', 'kk', 'km', 'kn', 'ko', 'lij', 'lt', 'lv',
488 'mai', 'mk', 'ml', 'mr', 'ms', 'nb-no', 'nl', 'nn-no', 'or',
489 'pa-in', 'pl', 'pt-br', 'pt-pt', 'rm', 'ro', 'ru', 'si', 'sk',
490 'sl', 'son', 'sq', 'sr', 'sv-se', 'ta', 'te', 'th', 'tr', 'uk',
491 'uz', 'vi', 'xh', 'zh-cn', 'zh-tw']
493 lang_code = self.settings.get('language_code')
494 lang_code = lang_code.replace('_', '-').lower()
495 if lang_code in lang_codes:
496 pkg_text = "firefox-i18n-{0}".format(lang_code)
497 logging.debug("Adding firefox language package (%s)", pkg_text)
498 self.packages.append(pkg_text)
500 def add_features(self):
501 """ Selects packages based on user selected features """
502 for xml_features in self.xml_root.iter('features'):
503 for xml_feature in xml_features.iter('feature'):
504 feature = xml_feature.attrib.get("name")
506 # If LEMP is selected, do not install lamp even if it's selected
507 if feature == "lamp" and self.settings.get("feature_lemp"):
510 # Add packages from each feature
511 if self.settings.get("feature_" + feature):
512 logging.debug("Adding packages for '%s' feature.", feature)
513 for pkg in xml_feature.iter('pkgname'):
514 if self.add_package(pkg):
516 "Selecting package %s for feature %s",
520 # Add libreoffice language package
521 if self.settings.get('feature_office'):
522 self.add_libreoffice_language()
524 # Add firefox language package
525 if self.settings.get('feature_firefox'):
526 self.add_firefox_language()