From eeed11eea953c6381a001cfad3adff93563a260b Mon Sep 17 00:00:00 2001 From: "Rafael G. Martins" Date: Sat, 18 Dec 2010 22:41:10 -0200 Subject: added new command line interface. --- g-octave | 19 +++ g_octave/cli.py | 406 ++++++++++++++++++++++++++++++++++++++++++++ g_octave/config.py | 12 +- g_octave/description.py | 13 +- g_octave/ebuild.py | 16 +- g_octave/exception.py | 18 +- g_octave/fetch.py | 13 +- g_octave/info.py | 10 +- g_octave/overlay.py | 4 +- g_octave/package_manager.py | 56 +++--- 10 files changed, 491 insertions(+), 76 deletions(-) create mode 100755 g-octave create mode 100644 g_octave/cli.py diff --git a/g-octave b/g-octave new file mode 100755 index 0000000..f79da18 --- /dev/null +++ b/g-octave @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + g_octave + ~~~~~~~~ + + g-octave's main script. + + :copyright: (c) 2010 by Rafael Goncalves Martins + :license: GPL-2, see LICENSE for more details. +""" + +from g_octave.cli import Cli +import sys + +if __name__ == '__main__': + cli = Cli() + sys.exit(cli.run()) diff --git a/g_octave/cli.py b/g_octave/cli.py new file mode 100644 index 0000000..0e18a09 --- /dev/null +++ b/g_octave/cli.py @@ -0,0 +1,406 @@ +# -*- coding: utf-8 -*- + +""" + g_octave.cli + ~~~~~~~~~~~~ + + This module implements a command-line interface for g-octave. + + :copyright: (c) 2010 by Rafael Goncalves Martins + :license: GPL-2, see LICENSE for more details. +""" + +from __future__ import absolute_import, print_function + +import argparse +import getpass +import os +import portage +import tempfile +import traceback +import shutil +import sys + +from .checksum import sha1_check_db +from .config import Config +from .description_tree import DescriptionTree +from .ebuild import Ebuild +from .exception import GOctaveError +from .fetch import fetch +from .log import Log +from .overlay import create_overlay +from .package_manager import get_by_name + +config = Config() +log = Log('g_octave.cli') +out = portage.output.EOutput() + + +class Cli: + + bug_tracker = 'https://bugs.gentoo.org/' + + def __init__(self): + log.info('Initializing g-octave.') + + self.parser = argparse.ArgumentParser( + description='A tool that generates and installs ebuilds for the octave-forge packages' + ) + + self.parser.set_defaults( + action=self.merge, + scm=(config.use_scm.lower() == 'true') + ) + + self.actions = self.parser.add_mutually_exclusive_group() + self.scm = self.parser.add_mutually_exclusive_group() + + self.actions.add_argument( + '--sync', + action = 'store_const', + const = self.sync, + dest = 'action', + help = 'search for updates of the package database, patches and auxiliary files.' + ) + + self.actions.add_argument( + '-l', '--list', + action = 'store_const', + const = self.list, + dest = 'action', + help = 'show a list of packages available to install (separed by categories) and exit.' + ) + + self.actions.add_argument( + '--list-raw', + action = 'store_const', + const = self.list_raw, + dest = 'action', + help = 'show a list of packages available to install (a package per line, without colors) and exit.' + ) + + self.actions.add_argument( + '-i', '--info', + action = 'store_const', + const = self.info, + dest = 'action', + help = 'show a description of the required package and exit.' + ) + + self.actions.add_argument( + '-u', '--update', + action = 'store_const', + const = self.update, + dest = 'action', + help = 'try to update a package or all the installed packages, if no atom provided.' + ) + + self.actions.add_argument( + '-s', '--search', + action = 'store_const', + const = self.search, + dest = 'action', + help = 'package search (regular expressions allowed).' + ) + + self.actions.add_argument( + '-C', '--unmerge', + action = 'store_const', + const = self.unmerge, + dest = 'action', + help = 'try to unmerge a package, instead of merge.' + ) + + self.actions.add_argument( + '--config', + action = 'store_const', + const = self.config, + dest = 'action', + help = 'return a value from the configuration file (/etc/g-octave.cfg)' + ) + + self.parser.add_argument( + '-p', '--pretend', + action = 'store_true', + dest = 'pretend', + help = 'don\'t (un)merge packages, only create ebuilds and solve the dependencies' + ) + + self.parser.add_argument( + '-a', '--ask', + action = 'store_true', + dest = 'ask', + help = 'ask for confirmation before perform (un)merges' + ) + + self.parser.add_argument( + '-v', '--verbose', + action = 'store_true', + dest = 'verbose', + help = 'package manager\'s makes a lot of noise.' + ) + + self.parser.add_argument( + '-1', '--oneshot', + action = 'store_true', + dest = 'oneshot', + help = 'do not add the packages to the world file for later updating.' + ) + + self.scm.add_argument( + '--scm', + action = 'store_true', + dest = 'scm', + help = 'enable the installation of the current live version of a package, if disabled on the configuration file' + ) + + self.scm.add_argument( + '--no-scm', + action = 'store_false', + dest = 'scm', + help = 'disable the installation of the current live version of a package, if enabled on the configuration file' + ) + + self.parser.add_argument( + '--force', + action = 'store_true', + dest = 'force', + help = 'forces the recreation of the ebuilds' + ) + + self.parser.add_argument( + '--force-all', + action = 'store_true', + dest = 'force_all', + help = 'forces the recreation of the overlay and of the ebuilds' + ) + + self.parser.add_argument( + '--no-colors', + action = 'store_false', + dest = 'colors', + help = 'don\'t use colors on the CLI' + ) + + self.parser.add_argument( + 'atom', + metavar='ATOM', + type=str, + nargs='?', + help='Package atom or regular expression (for search) or configuration key' + ) + + def _init_tree(self): + log.info('Initializing DescriptionTree.') + self.tree = DescriptionTree() + + def _required_atom(self): + if self.args.atom is None: + self.parser.error('You need to provide a positional argument') + + def _init_pkg_manager(self): + log.info('Initializing Package Manager.') + pm = get_by_name(config.package_manager) + if pm is None: + raise GOctaveError('Invalid package manager: %s' % config.package_manager) + self.pkg_manager = pm(self.args.ask, self.args.verbose, self.args.pretend, + self.args.oneshot, not self.args.colors) + + # checking if the package manager is installed + if not self.pkg_manager.is_installed(): + raise GOctaveError('Package manager not installed: %s' % config.package_manager) + + current_user = getpass.getuser() + if current_user not in self.pkg_manager.allowed_users(): + raise GOctaveError( + 'The current user (%s) can\'t run the current selected ' + 'package manager (%s)' % (current_user, config.package_manager) + ) + + # checking if the overlay is properly configured + self.pkg_manager.check_overlay(config.overlay, out): + raise GOctaveError('Overlay not properly configured.') + + def _init_ebuild(self): + log.info('Initializing Ebuild: %s' % self.args.atom) + self._required_atom() + self._init_pkg_manager() + self._init_overlay() + self.ebuild = Ebuild(self.args.atom, self.args.force, self.args.scm, \ + self.pkg_manager) + self.pkgatom = '=g-octave/' + self.ebuild.description.P + self.catpkg = 'g-octave/' + self.ebuild.description.PN + + def _init_overlay(self): + log.info('Initializing Overlay.') + create_overlay(self.args.force_all) + + def list(self): + log.info('Listing packages.') + self._init_tree() + print() + print(portage.output.blue('Available packages:')) + print() + packages = self.tree.list() + for category in packages: + print( + portage.output.blue('Category:'), + portage.output.white(category) + ) + print() + for pkg in packages[category]: + print( + portage.output.green(' Package:'), + portage.output.white(pkg) + ) + print( + portage.output.green(' Available versions:'), + portage.output.red(', '.join(packages[category][pkg])) + ) + print() + + def list_raw(self): + log.info('Listing packages (raw mode).') + self._init_tree() + packages = self.tree.list() + for cat in packages: + for pkg in packages[cat]: + print(pkg) + + def info(self): + log.info('Listing description of a package.') + self._init_ebuild() + pkg = self.ebuild.description + print(portage.output.blue('Package:'), portage.output.white(str(pkg.name))) + print(portage.output.blue('Version:'), portage.output.white(str(pkg.version))) + print(portage.output.blue('Date:'), portage.output.white(str(pkg.date))) + print(portage.output.blue('Maintainer:'), portage.output.white(str(pkg.maintainer))) + print(portage.output.blue('Description:'), portage.output.white(str(pkg.description))) + print(portage.output.blue('Categories:'), portage.output.white(str(pkg.categories))) + print(portage.output.blue('License:'), portage.output.white(str(pkg.license))) + print(portage.output.blue('Url:'), portage.output.white(str(pkg.url))) + + def update(self): + self._init_pkg_manager() + if self.args.atom is not None: + log.info('Calling the package manager to update the package.') + self._init_ebuild() + ret = self.pkg_manager.update_package(self.pkgatom, self.catpkg) + else: + log.info('Calling the package manager to update all the installed packages.') + ret = self.pkg_manager.update_package() + if ret != os.EX_OK: + raise GOctaveError('Update failed!') + + def search(self): + self._init_tree() + self._init_overlay() + self._required_atom() + log.info('Searching for packages: %s' % self.args.atom) + print( + portage.output.blue('Search results for '), + portage.output.white(self.args.atom), + portage.output.blue(':\n'), + sep = '' + ) + packages = self.tree.search(self.args.atom) + for pkg in packages: + print( + portage.output.green('Package:'), + portage.output.white(pkg) + ) + print( + portage.output.green('Available versions:'), + portage.output.red(', '.join(packages[pkg])) + ) + print() + + def merge(self): + self._init_pkg_manager() + self._init_ebuild() + self._required_atom() + log.info('Merging package: %s' % self.args.atom) + self.ebuild.create() + ret = self.pkg_manager.install_package(self.pkgatom, self.catpkg) + if ret != os.EX_OK: + raise GOctaveError('Merge failed!') + + def unmerge(self): + self._init_pkg_manager() + self._init_ebuild() + self._required_atom() + log.info('Unmerging package: %s' % self.args.atom) + self.ebuild.create() + ret = self.pkg_manager.uninstall_package(self.pkgatom, self.catpkg) + if ret != os.EX_OK: + raise GOctaveError('Unmerge failed!') + + def sync(self): + log.info('Searching updates ...') + out.einfo('Searching updates ...') + + if self.args.force and os.path.exists(config.db): + shutil.rmtree(config.db) + + if not self.updates.fetch_db(): + log.info('No updates available') + out.einfo('No updates available') + else: + self.updates.extract() + log.info('Checking SHA1 checksums ...') + out.ebegin('Checking SHA1 checksums') + self._init_tree() + if sha1_check_db(self.tree): + out.eend(0) + else: + out.eend(1) + if os.path.exists(config.db): + shutil.rmtree(config.db) + raise GOctaveError('Package database SHA1 checksum failed!') + + def config(self): + log.info('Retrieving configuration option.') + self._required_atom() + print(config.__getattr__(self.args.atom)) + + def _run(self): + log.info('Running the command-line interface.') + self.args = self.parser.parse_args() + if not self.args.colors: + portage.output.nocolor() + + self.updates = fetch() + + # validating the db_mirror + if self.updates is None: + raise GOctaveError('Your db_mirror value is invalid. Fix it, or leave empty to use the default.') + + # checking if we have a package database + if self.updates.need_update() and self.args.action != self.sync: + raise GOctaveError('No package database found! Please run `g-octave --sync`') + + self.args.action() + return os.EX_OK + + def run(self): + try: + return self._run() + except GOctaveError as err: + log.error(unicode(err)) + out.eerror(unicode(err)) + return os.EX_USAGE + except Exception as err: + tb = traceback.format_exc() + log.error(tb) + out.eerror('Unknown error - ' + unicode(err)) + fd, filename = tempfile.mkstemp(prefix='g-octave-', suffix='.log') + error_log = 'Command: ' + ' '.join(sys.argv) + '\n\n' + tb + if os.write(fd, error_log) != len(error_log): + out.eerror('Failed to save the traceback!') + else: + out.eerror('Traceback saved to: ' + filename) + out.eerror('Please report a bug with traceback and `emerge --info` attached.') + out.eerror('Bug tracker: ' + self.bug_tracker) + os.fchmod(fd, 0o777) + os.close(fd) + return os.EX_SOFTWARE diff --git a/g_octave/config.py b/g_octave/config.py index b0362cf..b454cd5 100644 --- a/g_octave/config.py +++ b/g_octave/config.py @@ -14,6 +14,8 @@ from __future__ import absolute_import import os +from .exception import GOctaveError + # py3k compatibility from .compat import py3k if py3k: @@ -21,11 +23,7 @@ if py3k: else: import ConfigParser as configparser -__all__ = ['Config', 'ConfigException'] - - -class ConfigException(Exception): - pass +__all__ = ['Config'] class Config(object): @@ -74,7 +72,7 @@ class Config(object): # no file to parsed if len(parsed_files) == 0: - raise ConfigException('File not found: %r' % config_file) + raise GOctaveError('File not found: %r' % config_file) def _evaluate_from_file(self, attr): # return the value from the configuration file @@ -97,4 +95,4 @@ class Config(object): # default to the configuration file return self._evaluate_from_file(attr) else: - raise ConfigException('Invalid option: %r' % attr) + raise GOctaveError('Invalid option: %r' % attr) diff --git a/g_octave/description.py b/g_octave/description.py index a242592..9937f1b 100644 --- a/g_octave/description.py +++ b/g_octave/description.py @@ -34,7 +34,7 @@ from contextlib import closing from .config import Config from .compat import py3k from .checksum import sha1_compute -from .exception import DescriptionException +from .exception import GOctaveError from .info import Info if py3k: @@ -73,7 +73,7 @@ class Description(object): if not os.path.exists(file): log.error('File not found: %s' % file) - raise DescriptionException('File not found: %s' % file) + raise GOctaveError('File not found: %s' % file) self._file = file self._info = Info(os.path.join(conf.db, 'info.json')) @@ -243,7 +243,7 @@ class Description(object): # invalid dependency atom else: log.error('Invalid dependency atom: %s' % depend) - raise DescriptionException('Invalid dependency atom: %s' % depend) + raise GOctaveError('Invalid dependency atom: %s' % depend) return list(set(depends_list)) @@ -277,7 +277,7 @@ class Description(object): # invalid dependency atom else: log.error('Invalid dependency atom: %s' % depend) - raise DescriptionException('Invalid dependency atom: %s' % depend) + raise GOctaveError('Invalid dependency atom: %s' % depend) return depends_list @@ -290,7 +290,8 @@ class Description(object): """method that overloads the object atributes, returning the needed atribute based on the dict with the previously parsed content. """ - + if name in ['depends', 'buildrequires', 'systemrequirements']: + return self._desc.get(name, []) return self._desc.get(name, None) @@ -310,7 +311,7 @@ class SvnDescription(Description): with open(temp_desc, 'wb') as fp_: shutil.copyfileobj(fp, fp_) except: - raise DescriptionException('Failed to fetch DESCRIPTION file from SVN') + raise GOctaveError('Failed to fetch DESCRIPTION file from SVN') Description.__init__(self, temp_desc) self.PN = package self.PV = '9999' diff --git a/g_octave/ebuild.py b/g_octave/ebuild.py index 371c9d4..9095f09 100644 --- a/g_octave/ebuild.py +++ b/g_octave/ebuild.py @@ -21,7 +21,7 @@ __all__ = [ from .config import Config from .description import * from .description_tree import DescriptionTree -from .exception import EbuildException +from .exception import GOctaveError from .compat import open import getpass @@ -96,15 +96,15 @@ class Ebuild: pkg_name = atom.group(1) version = atom.group(2) - self.description = self._tree.get(pkg_name + '-' + version) + self.description = self._tree.get('%s-%s' % (pkg_name, version)) if self._scm: if self.description is not None: self.description = SvnDescription(self.description.CAT, self.description.PN) else: - raise EbuildException('Failed to find the octave-forge category of this package.') + raise GOctaveError('Failed to find the octave-forge category of this package.') if self.description is None: - raise EbuildException('Package not found: %s' % pkg_atom) + raise GOctaveError('Package not found: %s' % pkg_atom) def create(self, display_info=True, accept_keywords=None, manifest=True, nodeps=False): ebuild_dir = os.path.join(config.overlay, 'g-octave', self.description.PN) @@ -127,11 +127,11 @@ class Ebuild: fp.write(METADATA_TEMPLATE % self._evaluate_metadata_vars()) if manifest: if self._pkg_manager.create_manifest(ebuild_file) != os.EX_OK: - raise EbuildException('Failed to create Manifest file!') + raise GOctaveError('Failed to create Manifest file!') except Exception as error: if display_info: out.eerror('Failed to create: g-octave/' + self.description.P + '.ebuild') - raise EbuildException(error) + raise GOctaveError(error) else: if not nodeps: self._resolve_dependencies() @@ -180,7 +180,7 @@ class Ebuild: for keyword in keywords: match = re_keywords.match(keyword) if match == None: - raise EbuildException('Invalid keyword: %s' % keyword) + raise GOctaveError('Invalid keyword: %s' % keyword) if match.group(1) == None: stable.append(match.group(2)) else: @@ -239,7 +239,7 @@ class Ebuild: to_install.append('%s-%s' % (pkg, self._tree.version_compare(allowed_versions))) if len(allowed_versions) == 0: - raise EbuildException('Can\'t resolve a dependency: %s' % pkg) + raise GOctaveError('Can\'t resolve a dependency: %s' % pkg) # creating the ebuilds for the dependencies, recursivelly for ebuild in to_install: diff --git a/g_octave/exception.py b/g_octave/exception.py index 1025a20..9f6f97c 100644 --- a/g_octave/exception.py +++ b/g_octave/exception.py @@ -11,22 +11,8 @@ :license: GPL-2, see LICENSE for more details. """ -__all__ = [ - 'DescriptionException', - 'DescriptionTreeException', - 'EbuildException', - 'FetchException', -] +__all__ = ['GOctaveError'] -class DescriptionException(Exception): - pass - -class DescriptionTreeException(Exception): - pass - -class EbuildException(Exception): - pass - -class FetchException(Exception): +class GOctaveError(Exception): pass diff --git a/g_octave/fetch.py b/g_octave/fetch.py index b43b585..69397bc 100644 --- a/g_octave/fetch.py +++ b/g_octave/fetch.py @@ -21,7 +21,7 @@ from .config import Config conf = Config() from .description_tree import DescriptionTree -from .exception import FetchException +from .exception import GOctaveError from .compat import py3k, open as open_ if py3k: @@ -71,9 +71,12 @@ class GitHub: branch ) commits = {} - with closing(urllib.urlopen(url)) as fp: - commits = json.loads(fp.read().decode('utf-8')) - return commits['commits'] + try: + with closing(urllib.urlopen(url)) as fp: + commits = json.loads(fp.read().decode('utf-8')) + return commits['commits'] + except: + raise GOctaveError('Failed to fetch the package database. Please check your internet connection.') def fetch_db(self, branch='master'): cache = os.path.join(conf.db, 'cache') @@ -119,7 +122,7 @@ class GitHub: fp.extractall(conf.db) dirs = glob.glob('%s/%s-%s*' % (conf.db, self.user, self.repo)) if len(dirs) != 1: - raise FetchException('Failed to extract the tarball.') + raise GOctaveError('Failed to extract the tarball.') return for f in os.listdir(dirs[0]): shutil.move(os.path.join(dirs[0], f), conf.db) diff --git a/g_octave/info.py b/g_octave/info.py index 7c873a6..338427a 100644 --- a/g_octave/info.py +++ b/g_octave/info.py @@ -14,14 +14,12 @@ from __future__ import absolute_import import json +from .exception import GOctaveError + # py3k compatibility from .compat import open -__all__ = ['Info', 'InfoException'] - - -class InfoException(Exception): - pass +__all__ = ['Info'] class Info(object): @@ -40,7 +38,7 @@ class Info(object): with open(filename) as fp: from_json = json.load(fp) except: - raise InfoException('Failed to load JSON file: %r' % filename) + raise GOctaveError('Failed to load JSON file: %r' % filename) else: if 'dependencies' in from_json: self.dependencies = from_json['dependencies'] diff --git a/g_octave/overlay.py b/g_octave/overlay.py index b7a267f..d25745b 100644 --- a/g_octave/overlay.py +++ b/g_octave/overlay.py @@ -20,7 +20,7 @@ import sys import shutil import portage.output -from .config import Config, ConfigException +from .config import Config from .compat import open config = Config() @@ -71,7 +71,7 @@ def create_overlay(force=False, quiet=False): if hasattr(content, 'name'): content = content.read() fp.write(content) - except Exception as error: + except: if not quiet: out.eend(1) sys.exit(1) diff --git a/g_octave/package_manager.py b/g_octave/package_manager.py index 6c804c3..7e992b6 100644 --- a/g_octave/package_manager.py +++ b/g_octave/package_manager.py @@ -15,6 +15,7 @@ __all__ = [ 'Portage', 'Pkgcore', 'Paludis', + 'get_by_name', ] import grp @@ -72,7 +73,6 @@ class Portage(Base): ] def __init__(self, ask=False, verbose=False, pretend=False, oneshot=False, nocolor=False): - self.overlay_bootstrap() self._fullcommand = [self._client] ask and self._fullcommand.append('--ask') verbose and self._fullcommand.append('--verbose') @@ -87,15 +87,15 @@ class Portage(Base): return self.run_command([pkgatom]) def uninstall_package(self, pkgatom, catpkg): - return self.run_command(['--unmerge', pkgatom]) + return self.run_command(['--unmerge', catpkg]) def update_package(self, pkgatom=None, catpkg=None): - if pkgatom is None: - pkgatom = self.installed_packages() + if catpkg is None: + catpkg = self.installed_packages() else: - pkgatom = [pkgatom] - self.do_ebuilds(pkgatom) - return self.run_command(['--update'] + pkgatom) + catpkg = [catpkg] + self.do_ebuilds(catpkg) + return self.run_command(['--update'] + catpkg) def installed_packages(self): packages = [] @@ -112,16 +112,10 @@ class Portage(Base): import portage if overlay not in portage.settings['PORTDIR_OVERLAY'].split(' '): out.eerror('g-octave overlay is not configured!') - out.eerror('You must append your overlay dir to PORTDIR_OVERLAY.') + out.eerror('You must append your overlay directory to PORTDIR_OVERLAY.') out.eerror('Overlay: %s' % overlay) return False return True - - def overlay_bootstrap(self): - overlay = conf.overlay - portdir_overlay = os.environ.get('PORTDIR_OVERLAY', '') - if overlay not in portdir_overlay: - os.environ['PORTDIR_OVERLAY'] = (portdir_overlay + ' ' + overlay).strip() class Pkgcore(Base): @@ -149,15 +143,15 @@ class Pkgcore(Base): return self.run_command([pkgatom]) def uninstall_package(self, pkgatom, catpkg): - return self.run_command(['--unmerge', pkgatom]) + return self.run_command(['--unmerge', catpkg]) def update_package(self, pkgatom=None, catpkg=None): - if pkgatom is None: - pkgatom = self.installed_packages() + if catpkg is None: + catpkg = self.installed_packages() else: - pkgatom = [pkgatom] - self.do_ebuilds(pkgatom) - return self.run_command(['--upgrade', '--noreplace'] + pkgatom) + catpkg = [catpkg] + self.do_ebuilds(catpkg) + return self.run_command(['--upgrade', '--noreplace'] + catpkg) def installed_packages(self): packages = [] @@ -216,19 +210,19 @@ class Paludis(Base): return self.run_command(cmd) def uninstall_package(self, pkgatom, catpkg): - return self.run_command(['--uninstall', pkgatom]) + return self.run_command(['--uninstall', catpkg]) def update_package(self, pkgatom=None, catpkg=None): - if pkgatom is None: - pkgatom = self.installed_packages() + if catpkg is None: + catpkg = self.installed_packages() else: - pkgatom = [pkgatom] - self.do_ebuilds(pkgatom) + catpkg = [catpkg] + self.do_ebuilds(catpkg) return self.run_command([ '--install', '--dl-upgrade', 'as-needed', '--dl-reinstall-targets', 'never', - ] + pkgatom) + ] + catpkg) def installed_packages(self): packages = [] @@ -243,3 +237,13 @@ class Paludis(Base): packages.append(line.strip()) return packages + +def get_by_name(name): + if name.lower() == 'portage': + return Portage + elif name.lower() == 'paludis': + return Paludis + elif name.lower() == 'pkgcore': + return Pkgcore + else: + return None -- cgit v1.2.3-65-gdbad