diff --git a/doc/config.docbook b/doc/config.docbook index 88009df..c2bd0ed 100644 --- a/doc/config.docbook +++ b/doc/config.docbook @@ -2,4 +2,5 @@ Configuration &config_bashrc; &config_set; +&config_hooks; diff --git a/doc/config/hooks.docbook b/doc/config/hooks.docbook new file mode 100644 index 0000000..c382815 --- /dev/null +++ b/doc/config/hooks.docbook @@ -0,0 +1,78 @@ + + Hooks Configuration + + Hooks Locations + + If a hook directory exists, they will either be executed before + or after that particular stage. The hooks inside each directory + will be executed by bash. Each one will receive the environment + of an ebuild, so they are capable of inherit, einfo, and other + common commands (if you find them useful). Avoid commands that + may trigger changes in the filesystem! + + + + A hook is presently not allowed to alter portage's execution, + but they can supplement it with additional functionality. Since + hooks execute in a bash environment, they are told the parent + process ID, which can be used to kill the parent (nicely, + please) if absolutely needed. This might be useful in a pre-sync + script. + + + + When a hook is called, any of the following arguments are + passed: + + /bin/bash ... + + --opt portage arguments, always translated to long form, given by user at the prompt, such as "--verbose" or "--newuse" + + --action a single action being performed by portage, such as "depclean", "sync", or an ebuild phase + + --target the thing to perform the action with or on + + + + + As of this writing, the following hook directories are + supported. It can be assumed that the above arguments apply + except wherever described differently. + + + + /etc/portage/hooks/pre-ebuild.d/ - executed before every ebuild execution. Never receives --opt, and --target is set to the full path of the ebuild. + /etc/portage/hooks/post-ebuild.d/ - yet to be implemented + /etc/portage/hooks/pre-run.d/ - executed before portage considers most things, including proper permissions and validity of arguments. + /etc/portage/hooks/post-run.d/ - executed after portage is done. It should run regardless of any errors or signals sent, but this cannot be guaranteed for certain scenarios (such as when the KILL signal is received). No information is available concerning the reason portage is exiting. This is a limitation of python itself. + /etc/portage/hooks/pre-sync.d/ - executed before portage synchronizes the portage tree. + /etc/portage/hooks/post-sync.d/ - executed after portage has successfully synchronized the portage tree. Presently you must use a combination of pre-sync and post-run to catch sync failures if desired. + + + + Skeleton Hook + + Most hooks will parse the options at the beginning and look for + specific things. This skeleton hook provides that functionality + to get you started. Replace the colons with actual code where + desired. + + + #!/bin/bash + + einfo "This is an example hook." + while [[ "$1" != "" ]]; do + if [[ "$1" == "--opt" ]]; then + : + elif [[ "$1" == "--action" ]]; then + : + elif [[ "$1" == "--target" ]]; then + : + else + ewarn "Unknown hook option: $1 $2" + fi + shift 2 + done + + + diff --git a/doc/portage.docbook b/doc/portage.docbook index 999103a..c754352 100644 --- a/doc/portage.docbook +++ b/doc/portage.docbook @@ -23,6 +23,7 @@ + ]> diff --git a/man/portage.5 b/man/portage.5 index 6c78cbd..b918411 100644 --- a/man/portage.5 +++ b/man/portage.5 @@ -62,6 +62,9 @@ repos.conf .BR /etc/portage/env/ package-specific bashrc files .TP +.BR /etc/portage/hooks/ +portage pre/post hooks +.TP .BR /etc/portage/profile/ site-specific overrides of \fB/etc/make.profile/\fR .TP @@ -637,6 +640,14 @@ order: /etc/portage/env/${CATEGORY}/${PF} .RE .TP +.BR /etc/portage/hooks/ +.RS +In this directory, portage hooks are executed before each ebuild phase, +before and after synchronization, and before and after portage runs +themselves. Please see the DocBook documentation for detailed +information. +.RE +.TP .BR /usr/portage/metadata/ .RS .TP diff --git a/pym/_emerge/actions.py b/pym/_emerge/actions.py index 148b8c3..052455c 100644 --- a/pym/_emerge/actions.py +++ b/pym/_emerge/actions.py @@ -33,6 +33,7 @@ from portage.output import blue, bold, colorize, create_color_func, darkgreen, \ red, yellow good = create_color_func("GOOD") bad = create_color_func("BAD") +from portage.hooks import HookDirectory from portage.sets import load_default_config, SETPREFIX from portage.sets.base import InternalPackageSet from portage.util import cmp_sort_key, writemsg, writemsg_level @@ -1817,6 +1818,7 @@ def action_sync(settings, trees, mtimedb, myopts, myaction): os.umask(0o022) dosyncuri = syncuri updatecache_flg = False + HookDirectory(phase='pre-sync', settings=settings, myopts=myopts, myaction=myaction).execute() if myaction == "metadata": print("skipping sync") updatecache_flg = True @@ -2260,6 +2262,8 @@ def action_sync(settings, trees, mtimedb, myopts, myaction): if retval != os.EX_OK: print(red(" * ") + bold("spawn failed of " + postsync)) + HookDirectory(phase='post-sync', settings=settings, myopts=myopts, myaction=myaction).execute() + if(mybestpv != mypvs) and not "--quiet" in myopts: print() print(red(" * ")+bold("An update to portage is available.")+" It is _highly_ recommended") diff --git a/pym/_emerge/main.py b/pym/_emerge/main.py index 9e91ee9..fa09544 100644 --- a/pym/_emerge/main.py +++ b/pym/_emerge/main.py @@ -27,6 +27,8 @@ bad = create_color_func("BAD") import portage.elog import portage.dep portage.dep._dep_check_strict = True +import portage.hooks +import portage.process import portage.util import portage.locks import portage.exception @@ -1233,6 +1235,11 @@ def emerge_main(): os.umask(0o22) settings, trees, mtimedb = load_emerge_config() portdb = trees[settings["ROOT"]]["porttree"].dbapi + + # Portage configured; let's let a hook set everything up before we do anything more + portage.process.atexit_register(portage.hooks.HookDirectory(phase='post-run', settings=settings, myopts=myopts, myaction=myaction, mytargets=myfiles).execute) + portage.hooks.HookDirectory(phase='pre-run', settings=settings, myopts=myopts, myaction=myaction, mytargets=myfiles).execute() + rval = profile_check(trees, myaction) if rval != os.EX_OK: return rval diff --git a/pym/portage/const.py b/pym/portage/const.py index 445677b..1fd48c3 100644 --- a/pym/portage/const.py +++ b/pym/portage/const.py @@ -35,6 +35,7 @@ CUSTOM_PROFILE_PATH = USER_CONFIG_PATH + "/profile" USER_VIRTUALS_FILE = USER_CONFIG_PATH + "/virtuals" EBUILD_SH_ENV_FILE = USER_CONFIG_PATH + "/bashrc" EBUILD_SH_ENV_DIR = USER_CONFIG_PATH + "/env" +HOOKS_PATH = USER_CONFIG_PATH + "/hooks" CUSTOM_MIRRORS_FILE = USER_CONFIG_PATH + "/mirrors" COLOR_MAP_FILE = USER_CONFIG_PATH + "/color.map" PROFILE_PATH = "etc/make.profile" diff --git a/pym/portage/hooks.py b/pym/portage/hooks.py new file mode 100644 index 0000000..6dd55e6 --- /dev/null +++ b/pym/portage/hooks.py @@ -0,0 +1,93 @@ +# Copyright 1998-2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# $Id$ + +# TODO: following may be harmful, but helpful for debugging +#import os, sys +#import os.path as osp +#sys.path.insert(0, osp.dirname(osp.dirname(osp.abspath(__file__)))) + +from portage.const import BASH_BINARY, HOOKS_PATH, PORTAGE_BIN_PATH +from portage import os +from portage import check_config_instance +from portage import normalize_path +from portage.exception import PortageException +from portage.exception import InvalidLocation +from portage.output import EOutput +from process import spawn + +class HookDirectory(object): + + def __init__ (self, phase, settings, myopts=None, myaction=None, mytargets=None): + self.myopts = myopts + self.myaction = myaction + self.mytargets = mytargets + check_config_instance(settings) + self.settings = settings + self.path = os.path.join(settings["PORTAGE_CONFIGROOT"], HOOKS_PATH, phase + '.d') + self.output = EOutput() + + def execute (self, path=None): + if not path: + path = self.path + + path = normalize_path(path) + + if not os.path.exists(path): + if self.myopts and "--debug" in self.myopts: + self.output.ewarn('This hook path could not be found; ignored: ' + path) + return + + if os.path.isdir(path): + for parent, dirs, files in os.walk(path): + for dir in dirs: + if self.myopts and "--debug" in self.myopts: + self.output.ewarn('Directory within hook directory not allowed; ignored: ' + path+'/'+dir) + for filename in files: + HookFile(os.path.join(path, filename), self.settings, self.myopts, self.myaction, self.mytargets).execute() + + else: + raise InvalidLocation('This hook path ought to be a directory: ' + path) + +class HookFile (object): + + def __init__ (self, path, settings, myopts=None, myaction=None, mytargets=None): + self.myopts = myopts + self.myaction = myaction + self.mytargets = mytargets + check_config_instance(settings) + self.path = normalize_path(path) + self.settings = settings + self.output = EOutput() + + def execute (self): + if "hooks" not in self.settings['FEATURES']: + return + + if not os.path.exists(self.path): + raise InvalidLocation('This hook path could not be found: ' + self.path) + + if os.path.isfile(self.path): + command=[self.path] + if self.myopts: + for myopt in self.myopts: + command.extend(['--opt', myopt]) + if self.myaction: + command.extend(['--action', self.myaction]) + if self.mytargets: + for mytarget in self.mytargets: + command.extend(['--target', mytarget]) + + command=[BASH_BINARY, '-c', 'source ' + PORTAGE_BIN_PATH + '/isolated-functions.sh && source ' + ' '.join(command)] + if self.myopts and "--verbose" in self.myopts: + self.output.einfo('Executing hook "' + self.path + '"...') + code = spawn(mycommand=command, env=self.settings.environ()) + if code: # if failure + raise PortageException('!!! Hook %s failed with exit code %s' % (self.path, code)) + + else: + raise InvalidLocation('This hook path ought to be a file: ' + self.path) + +if __name__ == "__main__": # TODO: debug + from portage.package.ebuild.config import config + HookDirectory('run', config()).execute() diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py index 531cb23..7370309 100644 --- a/pym/portage/package/ebuild/doebuild.py +++ b/pym/portage/package/ebuild/doebuild.py @@ -44,6 +44,7 @@ from portage.elog.messages import eerror, eqawarn from portage.exception import DigestException, FileNotFound, \ IncorrectParameter, InvalidAtom, InvalidDependString, PermissionDenied, \ UnsupportedAPIException +from portage.hooks import HookDirectory from portage.localization import _ from portage.manifest import Manifest from portage.output import style_to_ansi_code @@ -547,6 +548,8 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0, doebuild_environment(myebuild, mydo, myroot, mysettings, debug, use_cache, mydbapi) + HookDirectory(phase='pre-ebuild', settings=mysettings, myopts=None, myaction=mydo, mytargets=[mysettings["EBUILD"]]).execute() + if mydo in clean_phases: retval = spawn(_shell_quote(ebuild_sh_binary) + " clean", mysettings, debug=debug, fd_pipes=fd_pipes, free=1, @@ -601,9 +604,10 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0, mysettings["dbkey"] = \ os.path.join(mysettings.depcachedir, "aux_db_key_temp") - return spawn(_shell_quote(ebuild_sh_binary) + " depend", + retval = spawn(_shell_quote(ebuild_sh_binary) + " depend", mysettings, droppriv=droppriv) + return retval # Validate dependency metadata here to ensure that ebuilds with invalid # data are never installed via the ebuild command. Don't bother when @@ -650,8 +654,9 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0, del checkdir if mydo == "unmerge": - return unmerge(mysettings["CATEGORY"], + retval = unmerge(mysettings["CATEGORY"], mysettings["PF"], myroot, mysettings, vartree=vartree) + return retval # Build directory creation isn't required for any of these. # In the fetch phase, the directory is needed only for RESTRICT=fetch @@ -741,8 +746,9 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0, # if any of these are being called, handle them -- running them out of # the sandbox -- and stop now. if mydo == "help": - return spawn(_shell_quote(ebuild_sh_binary) + " " + mydo, + retval = spawn(_shell_quote(ebuild_sh_binary) + " " + mydo, mysettings, debug=debug, free=1, logfile=logfile) + return retval elif mydo == "setup": retval = spawn( _shell_quote(ebuild_sh_binary) + " " + mydo, mysettings, @@ -869,9 +875,11 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0, try: if mydo == "manifest": - return not digestgen(mysettings=mysettings, myportdb=mydbapi) + retval = digestgen(mysettings=mysettings, myportdb=mydbapi) + return not retval elif mydo == "digest": - return not digestgen(mysettings=mysettings, myportdb=mydbapi) + retval = digestgen(mysettings=mysettings, myportdb=mydbapi) + return not retval elif mydo != 'fetch' and not emerge_skip_digest and \ "digest" in mysettings.features: # Don't do this when called by emerge or when called just diff --git a/pym/portage/tests/hooks/__init__.py b/pym/portage/tests/hooks/__init__.py new file mode 100644 index 0000000..95dfcfc --- /dev/null +++ b/pym/portage/tests/hooks/__init__.py @@ -0,0 +1,5 @@ +# tests/portage/hooks/__init__.py -- Portage Unit Test functionality +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# $Id$ + diff --git a/pym/portage/tests/hooks/__test__ b/pym/portage/tests/hooks/__test__ new file mode 100644 index 0000000..e69de29 diff --git a/pym/portage/tests/hooks/test_HookDirectory.py b/pym/portage/tests/hooks/test_HookDirectory.py new file mode 100644 index 0000000..d19c47b --- /dev/null +++ b/pym/portage/tests/hooks/test_HookDirectory.py @@ -0,0 +1,49 @@ +# test_HookDirectory.py -- Portage Unit Testing Functionality +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# $Id$ + +from portage import os +from portage.hooks import HookDirectory +from portage.package.ebuild.config import config +from portage.tests import TestCase +from tempfile import mkdtemp +from shutil import rmtree + +# http://stackoverflow.com/questions/845058/how-to-get-line-count-cheaply-in-python +def file_len(fname): + with open(fname) as f: + for i, l in enumerate(f): + pass + return i + 1 + +class HookDirectoryTestCase(TestCase): + + def testHookDirectory(self): + """ + Tests to be sure a hook loads and reads the right settings + Based on test_PackageKeywordsFile.py + """ + + tmp_dir_path = self.BuildTmp('/etc/portage/hooks/test.d') + try: + settings = config() + settings["PORTAGE_CONFIGROOT"] = tmp_dir_path + settings["FEATURES"] += " hooks" + hooks = HookDirectory('test', settings) + hooks.execute() + self.assert_(file_len(tmp_dir_path+'/output') == 1) + finally: + rmtree(tmp_dir_path) + + def BuildTmp(self, tmp_subdir): + tmp_dir = mkdtemp() + hooks_dir = tmp_dir + '/' + tmp_subdir + os.makedirs(hooks_dir) + + f = open(hooks_dir+'/testhook', 'w') + f.write('#!/bin/bash\n') + f.write('echo hi > '+tmp_dir+'/output && exit 0\n') + f.close() + + return tmp_dir