OSDN Git Service

Initial commit
[rebornos/cnchi-gnome-osdn.git] / Cnchi / select_packages.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # select_packages.py
5 #
6 # Copyright © 2013-2018 Antergos
7 #
8 # This file is part of Cnchi.
9 #
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.
14 #
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.
19 #
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,
23 # MA 02110-1301, USA.
24
25 """ Package list generation module. """
26
27 import logging
28 import os
29 import subprocess
30 import requests
31 from requests.exceptions import RequestException
32
33 import xml.etree.cElementTree as elementTree
34
35 import desktop_info
36
37 import pacman.pac as pac
38
39 from misc.events import Events
40 import misc.extra as misc
41 from misc.extra import InstallError
42
43 import hardware.hardware as hardware
44
45 from lembrame.lembrame import Lembrame
46
47 # When testing, no _() is available
48 try:
49     _("")
50 except NameError as err:
51     def _(message):
52         return message
53
54
55 class SelectPackages():
56     """ Package list creation class """
57
58     PKGLIST_URL = 'https://gitlab.com/reborn-os-team/cnchi/blob/master/Cnchi/packages.xml'
59
60     def __init__(self, settings, callback_queue):
61         """ Initialize package class """
62
63         self.events = Events(callback_queue)
64         self.settings = settings
65         self.desktop = self.settings.get('desktop')
66
67         # Packages to be removed
68         self.conflicts = []
69
70         # Packages to be installed
71         self.packages = []
72
73         self.vbox = False
74
75         self.xml_root = None
76
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)
82
83     def create_package_list(self):
84         """ Create package list """
85
86         # Common vars
87         self.packages = []
88
89         logging.debug("Refreshing pacman databases...")
90         self.refresh_pacman_databases()
91         logging.debug("Pacman ready")
92
93         logging.debug("Selecting packages...")
94         self.select_packages()
95         logging.debug("Packages selected")
96
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")
101
102         if self.vbox:
103             self.settings.set('is_vbox', True)
104
105     @misc.raise_privileges
106     def refresh_pacman_databases(self):
107         """ Updates pacman databases """
108         # Init pyalpm
109         try:
110             pacman = pac.Pac(self.settings.get('pacman_config_file'), self.events.queue)
111         except Exception as ex:
112             template = (
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)
117
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)
123
124         try:
125             pacman.release()
126             del pacman
127         except Exception as ex:
128             template = (
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)
133
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
138
139         arch = pkg.attrib.get('arch')
140         if arch and arch != os.uname()[-1]:
141             return False
142
143         lang = pkg.attrib.get('lang')
144         locale = self.settings.get("locale").split('.')[0][:2]
145         if lang and lang != locale:
146             return False
147
148         lib = pkg.attrib.get('lib')
149         if lib and self.desktop not in libs[lib]:
150             return False
151
152         desktops = pkg.attrib.get('desktops')
153         if desktops and self.desktop not in desktops:
154             return False
155
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'))
162
163         # check conflicts attrib
164         conflicts = pkg.attrib.get('conflicts')
165         if conflicts:
166             self.add_conflicts(pkg.attrib.get('conflicts'))
167
168         # finally, add package
169         self.packages.append(pkg.text)
170         return True
171
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()
179         else:
180             logging.warning("Cannot find %s file", xml_filename)
181
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)
187         try:
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(
192                 url_error)
193             logging.warning(msg)
194
195     def load_xml_root_node(self):
196         """ Loads xml data, storing the root node """
197         self.xml_root = None
198
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)
203
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()
208
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)
214
215         if self.xml_root is None:
216             txt = "Could not load packages XML file (neither local nor from the Internet)"
217             logging.error(txt)
218             txt = _("Could not load packages XML file (neither local nor from the Internet)")
219             raise InstallError(txt)
220
221     def add_drivers(self):
222         """ Add package drivers """
223         try:
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()
229             if driver_names:
230                 logging.debug(
231                     "Hardware module detected these drivers: %s",
232                     driver_names)
233
234             # Add needed hardware packages to our list
235             hardware_pkgs = hardware_install.get_packages()
236             if hardware_pkgs:
237                 logging.debug(
238                     "Hardware module added these packages: %s",
239                     ", ".join(hardware_pkgs))
240                 if 'virtualbox' in hardware_pkgs:
241                     self.vbox = True
242                 self.packages.extend(hardware_pkgs)
243
244             # Add conflicting hardware packages to our conflicts list
245             self.conflicts.extend(hardware_install.get_conflicts())
246         except Exception as ex:
247             template = (
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)
251
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)
258
259         # Add ZFS filesystem
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)
265
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)
274
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:
288                 logging.warning(
289                     "Couldn't find %s bootloader packages!", boot_loader)
290
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()
296
297                 # Add common packages to all desktops (including base)
298                 if name == 'common':
299                     for pkg in edition.iter('pkgname'):
300                         self.add_package(pkg)
301
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)
306
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)
312
313     def maybe_add_vbox_packages(self):
314         """ Adds specific virtualbox packages if running inside a VM """
315         if self.vbox:
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')
319
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')
323
324     def select_packages(self):
325         """ Get package list from the Internet and add specific packages to it """
326         self.packages = []
327
328         # Load package list
329         self.load_xml_root_node()
330
331         # Add common and desktop specific packages
332         self.add_edition_packages()
333
334         # Add drivers' packages
335         self.add_drivers()
336
337         # Add file system packages
338         self.add_filesystems()
339
340         # Add chinese fonts (if necessary)
341         self.maybe_add_chinese_fonts()
342
343         # Add bootloader (if user chose it)
344         self.maybe_add_bootloader()
345
346         # Add extra virtualbox packages (if needed)
347         self.maybe_add_vbox_packages()
348
349         # Check for user desired features and add them to our installation
350         logging.debug(
351             "Check for user desired features and add them to our installation")
352         self.add_features()
353         logging.debug("All features needed packages have been added")
354
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()
360
361         # Remove duplicates and conflicting packages
362         self.cleanup_packages_list()
363         logging.debug("Packages list: %s", ','.join(self.packages))
364
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()
368
369     def check_packages(self):
370         """ Checks that all selected packages ARE in the repositories """
371         not_found = []
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]
378             try:
379                 output = subprocess.check_output(cmd).decode()
380             except subprocess.CalledProcessError:
381                 output = ""
382
383             if pkg_name not in output:
384                 not_found.append(pkg_name)
385                 logging.error("Package %s...NOT FOUND!", pkg_name)
386
387             percent = (index + 1) / num_pkgs
388             self.events.add('percent', percent)
389
390         if not_found:
391             txt = _("Cannot find these packages: {}").format(', '.join(not_found))
392             raise misc.InstallError(txt)
393
394
395     def cleanup_packages_list(self):
396         """ Cleans up a bit our packages list """
397         # Remove duplicates
398         self.packages = list(set(self.packages))
399         self.conflicts = list(set(self.conflicts))
400
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 != '']
404
405         # Remove any package from self.packages that is already in self.conflicts
406         if 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)
411
412     def add_conflicts(self, conflicts):
413         """ Maintains a list of conflicting packages """
414         if conflicts:
415             if ',' in conflicts:
416                 for conflict in conflicts.split(','):
417                     conflict = conflict.rstrip()
418                     if conflict not in self.conflicts:
419                         self.conflicts.append(conflict)
420             else:
421                 self.conflicts.append(conflicts)
422
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()
431         else:
432             # hunspell.txt not available, let's use this hardcoded version (as failsafe)
433             lang_codes = [
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']
439
440         if language_code in lang_codes:
441             pkg_text = "hunspell-{0}".format(language_code)
442             logging.debug(
443                 "Adding hunspell dictionary for %s language", pkg_text)
444             self.packages.append(pkg_text)
445         else:
446             logging.debug(
447                 "No hunspell language dictionary found for %s language code", language_code)
448
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']:
456                 code = locale
457             else:
458                 code = None
459         else:
460             # All the other language packs use their language code
461             code = self.settings.get('language_code')
462
463         if code:
464             code = code.replace('_', '-').lower()
465             pkg_text = "libreoffice-fresh-{0}".format(code)
466             logging.debug(
467                 "Adding libreoffice language package (%s)", pkg_text)
468             self.packages.append(pkg_text)
469             self.add_hunspell(code)
470
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()
479         else:
480             # Couldn't find firefox.txt, use this hardcoded version then (as failsafe)
481             lang_codes = [
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']
492
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)
499
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")
505
506                 # If LEMP is selected, do not install lamp even if it's selected
507                 if feature == "lamp" and self.settings.get("feature_lemp"):
508                     continue
509
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):
515                             logging.debug(
516                                 "Selecting package %s for feature %s",
517                                 pkg.text,
518                                 feature)
519
520         # Add libreoffice language package
521         if self.settings.get('feature_office'):
522             self.add_libreoffice_language()
523
524         # Add firefox language package
525         if self.settings.get('feature_firefox'):
526             self.add_firefox_language()