#!/usr/bin/env python3

try:
    import sys
    import os.path
    import shutil
    import optparse
    import time
    import traceback
    import distutils.sysconfig
    import subprocess
    import socket
    import re
    import bz2
    import smtplib
    from io                     import IOBase
except ImportError as e:
    module = str(e).split()[-1]


def getHost ():
    host       = 'unknown'
    hostname   = socket.gethostname()
    hostAddr   = socket.gethostbyname(hostname)
    if hostname == 'sl7' and hostAddr == '127.0.0.1':
        print( 'Running on <lepka>, enabling special packages tweaking.' )
        host = 'lepka'
    else:
        host = hostname.split('.')[0]
        if host.endswith('-wifi'):
            host = host[:-5]
    return host


class ErrorMessage ( Exception ):

    def __init__ ( self, code, *arguments ):
        self._code   = code
        self._errors = [ 'Malformed call to ErrorMessage()', '{}'.format(arguments) ]
        text = None
        if len(arguments) == 1:
            if isinstance(arguments[0],Exception): text = str(arguments[0]).split('\n')
            else:
                self._errors = arguments[0]
        elif len(arguments) > 1:
            text = list(arguments)
        if text:
            self._errors = []
            while len(text[0]) == 0: del text[0]
            lstrip = 0
            if text[0].startswith('[ERROR]'): lstrip = 8
            for line in text:
                if line[0:lstrip  ] == ' '*lstrip or \
                   line[0:lstrip-1] == '[ERROR]':
                    self._errors += [ line[lstrip:] ]
                else:
                    self._errors += [ line.lstrip() ]
        return

    def __str__ ( self ):
        if not isinstance(self._errors,list):
            return "[ERROR] {}".format(self._errors)
        formatted = "\n"
        for i in range(len(self._errors)):
            if i == 0: formatted += "[ERROR] {}".format(self._errors[i])
            else:      formatted += "        {}".format(self._errors[i])
            if i+1 < len(self._errors): formatted += "\n"
        return formatted

    def addMessage ( self, message ):
        if not isinstance(self._errors,list):
            self._errors = [ self._errors ]
        if isinstance(message,list):
            for line in message:
                  self._errors += [ line ]
        else:
            self._errors += [ message ]
        return

    def terminate ( self ):
        print( self )
        sys.exit(self._code)

    @property
    def code ( self ): return self._code


class BadBinary ( ErrorMessage ):

    def __init__ ( self, binary ):
        ErrorMessage.__init__( self, 1, 'Binary not found: "{}".'.format(binary) )
        return


class BadReturnCode ( ErrorMessage ):

    def __init__ ( self, status ):
        ErrorMessage.__init__( self, 1, 'Command returned status:{}.'.format(status) )
        return


class InstallFail ( ErrorMessage ):

    def __init__ ( self, packages ):
        ErrorMessage.__init__( self, 1, 'Failed to install packages:{}.'.format(packages) )


class Command ( object ):

    def __init__ ( self, arguments, fdLog=None, fdFilter=None ):
        self.arguments = arguments
        self.fdLog     = fdLog
        self.fdFilter  = fdFilter
        if self.fdLog != None and not isinstance(self.fdLog,IOBase):
            print( '[WARNING] Command.__init__(): "fdLog" is neither None or a file.' )
        return

    def _argumentsToStr ( self, arguments ):
        s = ''
        for argument in arguments:
            if argument.find(' ') >= 0: s += ' "' + argument + '"'
            else:                       s += ' '  + argument
        return s

    def log ( self, text ):
        line = None
        if isinstance(text,bytes):
            line = text.decode('utf-8')
        elif isinstance(text,str):
            line = text
        else:
            print( '[ERROR] Command.log(): "text" is neither bytes or str.' )
            print( '        {}'.format(text) )
        if line is not None:
            if isinstance(self.fdLog,IOBase):
                if self.fdFilter:
                    self.fdFilter.logFilter( line )
                self.fdLog.write( line )
                self.fdLog.flush()
            else:
                print( line[:-1] )
        sys.stdout.flush()
        sys.stderr.flush()
        return

    def execute ( self ):
        global conf
        sys.stdout.flush()
        sys.stderr.flush()
        
        homeDir = os.environ['HOME']
        workDir = os.getcwd()
        if homeDir.startswith(homeDir):
            workDir = '~' + workDir[ len(homeDir) : ]
        user = 'root'
        if 'USER' in os.environ: user = os.environ['USER']
        prompt = '{}@{}:{}$'.format(user,'fox',workDir)
        
        try:
            self.log( '{}{}\n'.format(prompt,self._argumentsToStr(self.arguments)) )
            #print( self.arguments )
            child = subprocess.Popen( self.arguments, stdout=subprocess.PIPE, stderr=subprocess.STDOUT )
            while True:
                line = child.stdout.readline()
                if not line: break
                self.log( line )
        except OSError as e:
            raise BadBinary( self.arguments[0] )
        
        (pid,status) = os.waitpid( child.pid, 0 )
        status >>= 8
        return status


class CommandArg ( object ):

    def __init__ ( self, command, wd=None, fdLog=None ):
        self.command = command
        self.wd      = wd
        self.fdLog   = fdLog
        return

    def __str__ ( self ):
        s = ''
        if self.wd: s = 'cd {} && '.format(self.wd)
        for i in range(len(self.command)):
            if i: s += ' '
            s += self.command[i]
        return s

    def logFilter ( self, line ):
        pass

    def getArgs ( self ):
        return self.command

    def execute ( self ):
        if self.wd: os.chdir( self.wd )
        return Command( self.getArgs(), self.fdLog, fdFilter=self ).execute()


class Rpmbuild ( CommandArg ):

    reDepends = re.compile( r'\s*(?P<depend>[\S]+).*is needed by.*' )
    rePackage = re.compile( r'^Wrote: (?P<package>[\S]+\.rpm)$' )

    def __init__ ( self, command, fdLog=None ):
        CommandArg.__init__ ( self, command, fdLog=fdLog )
        self.depends     = []
        self.binaries    = []
        self.debugsource = []
        self.debuginfo   = []
        self.source      = []
        return

    def logFilter ( self, line ):
        print( line[:-1] )
        m = Rpmbuild.reDepends.match( line )
        if m:
            self.depends.append( m.group('depend') )
        else:
            m = Rpmbuild.rePackage.match( line )
            if m:
                package = m.group('package')
                if package.endswith('src.rpm'):
                    self.source.append( package )
                elif package.find('debugsource') != -1:
                    self.debugsource.append( package )
                elif package.find('debuginfo') != -1:
                    self.debuginfo.append( package )
                else:
                    self.binaries.append( package )


class Repository ( object ):

    repos = []

    @staticmethod
    def find ( packName ):
        for repo in Repository.repos:
            if packName in repo.packages:
                return repo, repo.packages[ packName ]
        return None, None

    def __init__ ( self, rootDir, disttag ):
        self.rootDir  = rootDir
        self.disttag  = disttag
        self.packages = {}
        for root, dirs, files in os.walk(rootDir):
            for file in files:
                if not file.endswith('.src.rpm'):
                    continue
                m = reNVRSP.match( file )
                if m:
                    if m.group('name') in self.packages:
                       self.packages[ m.group('name') ].append( 
                            ( root + '/' + file
                            , m.group('name')
                            , m.group('v')
                            , m.group('r')
                            ))
                    else:
                       self.packages[ m.group('name') ] \
                          = [ ( root + '/' + file
                              , m.group('name')
                              , m.group('v')
                              , m.group('r')
                              ) ]
        Repository.repos.append( self )

    def show ( self ):
        print( 'Repository "{}"'.format( self.rootDir ))
        print( 'Contains {} source packages.'.format( len(self.packages) ))
        for name, srpms in self.packages.items():
            print( '{}: {}'.format( name, srpms[0] ))


if __name__ == '__main__':
    host = getHost()

    dnfClean = [ 'dnf', 'clean', 'all' ]
    CommandArg( dnfClean ).execute()

    dnfUpdate = [ 'dnf', '--assumeyes', 'update' ]
    CommandArg( dnfUpdate ).execute()

    systemPackages  = \
        [ 'grub2-tools-extra', 'libnsl', 'firewall-config'
        , 'ypbind', 'yp-tools', 'autofs', 'nfs-utils', 'postfix'
        , 'ntfs-3g', 'ntfsprogs', 'rsync', 'p7zip'
        , 'ncurses-compat-libs'
        , 'nut-client', 'nrpe', 'ocsinventory-agent', 'rsyslog', 'htop'
        , 'rusers-server', 'finger-server', 'cups', 'cups-client'
        , 'uucp', 'lrzsz', 'libatomic', 'python-unversioned-command'
        , 'python3-dnf-plugin-versionlock', 'dnf-automatic'
        , 'python3-rrdtool', 'rpm-build', 'rpm-sign', 'createrepo_c'
        , 'qt5-qtgraphicaleffects', 'qt5-qtquickcontrols2', 'qt5ct', 'kvantum'
        , 'sddm', 'sddm-themes', 'sddm-x11', 'gdm-tools'
        , 'icewm', 'icewm-themes'
        , 'xscreensaver-gl-extras', 'network-manager-applet'
        , 'tcsh', 'zsh', 'ksh', 'mutt', 'screen', 'google-chrome-stable'
        , 'xpdf', 'xdotool'
        , 'gtk-vnc2', 'gvnc', 'tigervnc', 'tigervnc-server'
        , 'gnuplot', 'python-doit'
        , 'fetchmail', 'procmail'
        , 'emacs', 'emacs-nox', 'thunderbird'
        , 'ImageMagick', 'graphviz', 'inkscape'
        , 'evince', 'atril', 'okular', 'pdftk'
        , 'python3-matplotlib-qt5', 'python3-matplotlib-gtk3', 'python3-matplotlib-doc'
        , 'python3-pandas', 'python-xdot', 'python3-networkx'
        , 'rdesktop', 'pcsc-lite'
       #, 'VirtualBox-7.0'
        ]
    windowsPackages = \
        [ 'fvwm', 'vim-X11', 'vim-enhanced', 'Qogir-theme', 'desktop-themes-soc'
        , 'dconf-editor'
        , 'gnome-tweaks', 'gnome-encfs-manager'
        , 'libreoffice', 'libreoffice-langpack-en', 'libreoffice-langpack-fr'
        # Whole MATE environment. Should be put into a packaging group (comps).
        , 'f35-backgrounds-extras-mate', 'f35-backgrounds-mate', 'f36-backgrounds-extras-mate'
        , 'f36-backgrounds-mate', 'f37-backgrounds-extras-mate', 'f37-backgrounds-mate'
        , 'imsettings-mate', 'libmatekbd', 'libmatekbd-devel', 'libmatemixer', 'libmatemixer-devel'
        , 'libmateweather', 'libmateweather-data', 'libmateweather-devel', 'mate-applets'
        , 'mate-backgrounds', 'mate-calc', 'mate-common', 'mate-control-center'
        , 'mate-control-center-devel', 'mate-control-center-filesystem', 'mate-desktop'
        , 'mate-desktop-configs', 'mate-desktop-devel', 'mate-desktop-libs', 'mate-dictionary'
        , 'mate-disk-image-mounter', 'mate-disk-usage-analyzer', 'mate-icon-theme', 'mate-media'
        , 'mate-menu', 'mate-menus', 'mate-menus-devel', 'mate-menus-libs'
        , 'mate-menus-preferences-category-menu', 'mate-notification-daemon', 'mate-panel'
        , 'mate-panel-devel', 'mate-panel-libs', 'mate-polkit', 'mate-power-manager'
        , 'mate-screensaver', 'mate-screensaver-devel', 'mate-screenshot', 'mate-search-tool'
        , 'mate-sensors-applet', 'mate-sensors-applet-devel', 'mate-session-manager'
        , 'mate-settings-daemon', 'mate-settings-daemon-devel', 'mate-system-log'
        , 'mate-system-monitor', 'mate-terminal', 'mate-themes', 'mate-user-admin'
        , 'mate-user-guide', 'mate-utils', 'mate-utils-common', 'mate-utils-devel'
        , 'material-design-dark', 'material-design-light', 'slick-greeter-mate', 'xapps-mate'
        , 'xmonad-mate', 'marco'
        , 'caja', 'caja', 'caja-actions', 'caja-actions-doc', 'caja-beesu'
        , 'caja-core-extensions', 'caja-extensions-common', 'caja-image-converter'
        , 'caja-open-terminal', 'caja-schemas', 'caja-sendto', 'caja-share'
        , 'caja-wallpaper', 'caja-xattr-tags', 'seahorse-caja'
        # Whole XFCE environment. Should be put into a packaging group (comps).
        , 'xfce-polkit', 'xfce4-appfinder', 'xfce4-panel', 'xfce4-power-manager'
        , 'xfce4-pulseaudio-plugin', 'xfce4-screensaver', 'xfce4-session', 'xfce4-settings'
        , 'xfce4-terminal', 'xfconf', 'xfdesktop', 'xfwm4', 'xfce-theme-manager'
        , 'xfce4-about', 'xfce4-battery-plugin', 'xfce4-calculator-plugin'
        , 'xfce4-clipman-plugin', 'xfce4-cpufreq-plugin', 'xfce4-cpugraph-plugin'
        , 'xfce4-datetime-plugin', 'xfce4-dev-tools', 'xfce4-dict', 'xfce4-dict-plugin'
        , 'xfce4-diskperf-plugin', 'xfce4-eyes-plugin', 'xfce4-fsguard-plugin'
        , 'xfce4-genmon-plugin', 'xfce4-mailwatch-plugin', 'xfce4-mount-plugin'
        , 'xfce4-netload-plugin', 'xfce4-notes-plugin', 'xfce4-notifyd', 'xfce4-panel-devel'
        , 'xfce4-panel-profiles', 'xfce4-places-plugin', 'xfce4-screenshooter'
        , 'xfce4-screenshooter-plugin', 'xfce4-sensors-plugin', 'xfce4-sensors-plugin-devel'
        , 'xfce4-smartbookmark-plugin', 'xfce4-statusnotifier-plugin', 'xfce4-systemload-plugin'
        , 'xfce4-taskmanager', 'xfce4-time-out-plugin', 'xfce4-timer-plugin', 'xfce4-verve-plugin'
        , 'xfce4-volumed', 'xfce4-wavelan-plugin', 'xfce4-weather-plugin'
        , 'xfce4-whiskermenu-plugin', 'xfce4-xkb-plugin', 'xfconf-devel', 'xfdashboard'
        , 'xfdashboard-devel', 'xfdashboard-themes', 'xfwm4-themes', 'Thunar'
        , 'thunar-archive-plugin', 'thunar-volman', 'Thunar-docs', 'thunar-sendto-clamtk'
        , 'thunar-vcs-plugin', 'thunarx-python', 'tumbler-extras'
        , 'gnome-themes-soc'
        ]
    programmingPackages = \
        [ 'git-all', 'git-lfs'
        , 'ccache', 'binutils-devel', 'rapidjson-devel', 'boost-devel'
        , 'gdb', 'clang', 'clang-tools-extra', 'libstdc++-static'
        , 'python3-devel', 'python3-docs',  'libxml2-devel', 'bzip2-devel'
        , 'libXt-devel', 'libXpm-devel', 'motif-devel'
        , 'expat-devel', 'gmp-devel', 'tcl-devel', 'libedit-devel'
        , 'readline-devel', 'mpfr-devel'
        , 'gobject-introspection-devel', 'perf'
        , 'qt5-qtsvg-devel', 'qwt-qt5-devel'
        , 'imake', 'automake', 'libtool', 'bison', 'flex', 'global'
        , 'cmake', 'meson', 'python3-tomli', 'python3-platformdirs'
        , 'doxygen', 'doxygen-doxywizard', 'doxygen-latex', 'doxygen2man'
        , 'python3-sphinx', 'python-sphinx-doc', 'python3-readthedocs-sphinx-ext'
        , 'python-sphinx_rtd_theme-doc', 'python3-sphinx-bootstrap-theme'
        , 'python3-pelican'
        , 'meld', 'code', 'geany'
        , 'opencv-devel', 'python3-opencv'
        , 'coin-or-lemon-devel', 'eigen3-devel'
        , 'qemu-kvm', 'dtc'
        , 'rpmdevtools', 'lcov', 'xz-lzma-compat', 'libusb-devel'
        , 'gcc-avr32-linux-gnu', 'arm-none-eabi-gcc-cs', 'SDL2-devel'
        , 'gperf'
        ]
    texlivePackages = \
        [ 'xfig', 'dspdfviewer'
        , 'fontawesome-fonts-web', 'texlive-fontawesome5', 'texlive-noto'
        , 'texlive-fontaxes'
        , 'texlive-lstaddons', 'texinfo', 'latexmk', 'rubber', 'texlive-pdfjam'
        , 'latex2html', 'ghostscript-tools-dvipdf'
        , 'texlive-collection-latex'
        , 'texlive-epstopdf-bin', 'texlive-eulervm', 'texlive-pgf-pie', 'texlive-pgf-blur'
        , 'texlive-opensans', 'texlive-subfigure', 'texlive-wrapfig', 'texlive-titlesec'
        , 'texlive-pgf', 'texlive-adjustbox', 'texlive-pgfplots', 'texlive-todonotes'
        , 'texlive-multirow', 'texlive-boxedminipage', 'texlive-standalone'
        , 'texlive-dblfloatfix', 'texlive-mathabx', 'texlive-mathabx-type1'
        , 'texlive-lipsum', 'texlive-xypic', 'texlive-thmtools'
        , 'texlive-algorithmicx', 'texlive-makecell', 'texlive-vmargin'
        , 'texlive-appendixnumberbeamer'
        , 'texlive-ae-doc', 'texlive-appendix', 'texlive-arabxetex', 'texlive-arphic'
        , 'texlive-bibtopic', 'texlive-bidi', 'texlive-bigfoot', 'texlive-changebar'
        , 'texlive-changepage', 'texlive-cjk', 'texlive-cns', 'texlive-collection-xetex'
        , 'texlive-datetime', 'texlive-eepic', 'texlive-epsf', 'texlive-euenc'
        , 'texlive-fixlatvian', 'texlive-fmtcount', 'texlive-fontbook', 'texlive-fontwrap'
        , 'texlive-garuda-c90', 'texlive-hyphenat', 'texlive-ifmtarg', 'texlive-iftex'
        , 'texlive-lastpage', 'texlive-lettrine', 'texlive-makecmds', 'texlive-mathspec'
        , 'texlive-mnsymbol', 'texlive-ncctools', 'texlive-norasi-c90', 'texlive-overpic'
        , 'texlive-philokalia', 'texlive-placeins', 'texlive-polyglossia', 'texlive-preprint'
        , 'texlive-ptext', 'texlive-realscripts', 'texlive-sectsty', 'texlive-stmaryrd'
        , 'texlive-t2', 'texlive-textpos', 'texlive-ucharclasses'
        , 'texlive-uhc', 'texlive-unisugar', 'texlive-wadalab', 'texlive-was'
        , 'texlive-xecjk', 'texlive-xecolor', 'texlive-xecyr', 'texlive-xeindex'
        , 'texlive-xepersian', 'texlive-xesearch', 'texlive-xetex', 'texlive-xetex-bin'
        , 'texlive-xetex-def', 'texlive-xetex-itrans', 'texlive-xetex-pstricks'
        , 'texlive-xetex-tibetan', 'texlive-xetexconfig', 'texlive-xetexfontinfo'
        , 'texlive-xifthen', 'texlive-xltxtra', 'texlive-xstring'
        , 'texlive-a4wide', 'texlive-aeguill', 'texlive-bophook', 'texlive-comment'
        , 'texlive-exam', 'texlive-examplep', 'texlive-exercise', 'texlive-glossaries-doc'
        , 'texlive-moreverb', 'texlive-relsize', 'texlive-tikz-qtree', 'texlive-upquote'
        , 'texlive-xcomment', 'texlive-minitoc', 'texlive-glossaries', 'texlive-draftcopy'
        , 'texlive-storebox', 'texlive-picinpar', 'texlive-circuitikz', 'texlive-anyfontsize'
        , 'texlive-babel-french', 'texlive-biblatex', 'texlive-yfonts', 'texlive-kpfonts'
        , 'texlive-hyphen-french'
        , 'texlive-fourier', 'texlive-fouriernc'
        , 'texlive-mathdots', 'texlive-beamerposter'
        , 'texlive-subfloat', 'texlive-emptypage', 'texlive-acronym', 'texlive-nomencl'
        , 'texlive-lettre', 'texlive-lettre-doc' 
        , 'texlive-graphviz', 'texlive-doublestroke', 'texlive-changes', 'texlive-minted'
        , 'texlive-shadow', 'texlive-threeparttable', 'texlive-chronology'
        , 'texlive-arydshln', 'texlive-nth'
        # TikZ.
        , 'texlive-tikz-timing', 'texlive-tikzmark'
        # Lots of additonal symbol/fonts.
        , 'texlive-tipa', 'texlive-wsuipa', 'texlive-wasysym', 'texlive-phonetic'
        , 'texlive-fc', 'texlive-jknapltx', 'texlive-metre', 'texlive-arcs'
        , 'texlive-fetamont', 'texlive-marvosym', 'texlive-fontawesome', 'texlive-teubner'
        , 'texlive-eurosym', 'texlive-cclicenses', 'texlive-ccicons', 'texlive-mathabx'
        , 'texlive-mnsymbol', 'texlive-boisik', 'texlive-stix', 'texlive-mathdesign'
        , 'texlive-cmll', 'texlive-shuffle', 'texlive-esint', 'texlive-bigints'
        , 'texlive-prodint', 'texlive-stmaryrd', 'texlive-turnstile'
        , 'texlive-trsym', 'texlive-trfsigns', 'texlive-oberdiek', 'texlive-harpoon'
        , 'texlive-chemarrow', 'texlive-fge', 'texlive-was', 'texlive-nath'
        , 'texlive-yhmath', 'texlive-fragments', 'texlive-esvect'
        , 'texlive-ushort', 'texlive-mdwtools', 'texlive-actuarialangle', 'texlive-extpfeil'
        , 'texlive-dotarrow', 'texlive-begriff', 'texlive-mathcomp', 'texlive-frege'
        , 'texlive-dozenal', 'texlive-ifsym', 'texlive-starfont', 'texlive-keystroke'
        , 'texlive-ascii-font', 'texlive-bbding', 'texlive-adfsymbols', 'texlive-adforn'
        , 'texlive-arev', 'texlive-bbding', 'texlive-dingbat', 'texlive-universa'
        , 'texlive-manfnt', 'texlive-clock', 'texlive-epsdice', 'texlive-universa'
        , 'texlive-bullcntr', 'texlive-skull', 'texlive-skak', 'texlive-dictsym'
        , 'texlive-recycle', 'texlive-countriesofeurope'
        , 'texlive-abstract', 'texlive-epigraph', 'texlive-framed', 'texlive-shorttoc'
        , 'texlive-tablefootnote', 'texlive-tcolorbox', 'texlive-wallpaper'
        , 'texlive-context-fullpage'
        ]
    multimediaPackages = \
        [ 'gimp', 'obs-studio', 'cantata', 'vlc', 'mplayer', 'grip', 'abcde'
        , 'zoom', 'teams', 'webex'
        ]
    vlsiPackages = \
        [ 'magic', 'xschem', 'xschem-doc'
        , 'yosys', 'python3-yosys'
        , 'klayout', 'ngspice', 'libngspice'
        , 'iverilog', 'ghdl'
        , 'openpdks-c4m-gf180mcu'
        ]
    serverPackages = \
        [ 'nut', 'nut-client'
        ]

    adminPackages = []
    if host in [ 'melon', 'lepka' ]:
        adminPackages = \
            [ 'ansible', 'ansible-collection-ansible-posix'
            , 'amanda-client', 'amanda-server'
            ]
    laptopPackages = []
    if host in [ 'lepka' ]:
        laptopPackages = [ 'mpd' ]

    enabledRepos = []
    hostPackages = []
    if host in [ 'plcian12', 'lepka', 'cole', 'fox', 'trot' ]:
        enabledRepos.append( 'cuda-local' )
        hostPackages += [ 'cuda-12-1' ]

    if host in [ 'folk', 'rollins', 'banjo', 'miller', 'gabrielli' ]:
        hostPackages += [ 'VirtualBox', 'VirtualBox-kmodsrc', 'akmod-VirtualBox'
                        , 'kmod-VirtualBox'
                        ]
    if host in [ 'shadwaya' ]:
        hostPackages += [ 'VirtualBox-7.0', 'langpacks-zh_CN' ]

    if host in [ 'jamal' ]:
        hostPackages += [ 'wine' ]

    if host in [ 'rolland' ]:
        hostPackages += [ 'cerbot', 'mod_ssl', 'python3-certbot-apache'
                        , 'fail2ban'
                        ]

    excludePackages = [ 'ffmpeg-free', 'ffmpeg-free-devel' ]

    packages = systemPackages      \
             + windowsPackages     \
             + programmingPackages \
             + texlivePackages     \
             + multimediaPackages  \
             + vlsiPackages        \
             + serverPackages      \
             + adminPackages       \
             + laptopPackages      \
             + hostPackages
    dnfInstall = [ 'dnf', '--assumeyes', '--nobest' ]
    if enabledRepos:
        dnfInstall.append( '--enablerepo=' + ','.join( enabledRepos ))
   #dnfInstall += [ '--exclude', ','.join(excludePackages) ]
    dnfInstall += [ 'install' ] + packages
    status = CommandArg( dnfInstall ).execute()
    if status:
         raise InstallFail( packages )

