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..ca62f5f --- /dev/null +++ b/doc/config/hooks.docbook @@ -0,0 +1,97 @@ + + Hooks Configuration + + Hooks Locations + + If a hook directory exists, the bash scripts within each one + wil either be executed before or after that particular stage, in + alphabetical order. 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! + + + + All hooks are not allowed to directly alter portage's execution, + but they can accomplish certain extra tasks at various points, + which might indrectly alter portage's execution. Since hooks + execute in a bash environment, they are told the parent process + ID, which can be used to kill portage if absolutely needed. This + might be useful if a hook handled the rest of a certain job, + such as syncing, and portage's default behavior is undesired, or + if a hook caught potential problems with the rest of portage's + execution. + + + + A hook script is expected to understand the following usage: + + /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 + + + + + The following hook directories are supported. The standard hook + script usage applies, 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/ - executed after every ebuild execution. Never receives --opt, and --target is set to the full path of the ebuild. + /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. + + + It's highly recommended that --verbose, --debug, and --quiet be + utilized for suppressing or adding to "regular" output. The + following skeleton hook already has example code in place to + handle these flags. + + + #!/bin/bash + + verbose_redirect="/dev/null" + debug_redirect="/dev/null" + while [[ "$1" != "" ]]; do + if [[ "$1" == "--opt" ]]; then + if [[ "$2" == "--verbose" ]]; then + verbose_redirect="/dev/tty" + fi + if [[ "$2" == "--debug" ]]; then + debug_redirect="/dev/tty" + fi + if [[ "$2" == "--quiet" ]]; then + verbose_redirect="/dev/null" + debug_redirect="/dev/null" + fi + elif [[ "$1" == "--action" ]]; then + : + elif [[ "$1" == "--target" ]]; then + : + else + ewarn "Unknown hook option: $1 $2" > "${verbose_redirect}" 2>&1 + fi + shift 2 + done + einfo "This is an example hook." > "${verbose_redirect}" 2>&1 + einfo "This is debug output." > "${debug_redirect}" 2>&1 + + + 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..91b2b05 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 @@ -1232,7 +1234,16 @@ def emerge_main(): # Portage needs to ensure a sane umask for the files it creates. os.umask(0o22) settings, trees, mtimedb = load_emerge_config() + + # Portage configured; let's let hooks run before we do anything more + portage.hooks.HookDirectory(phase='pre-run', settings=settings, myopts=myopts, myaction=myaction, mytargets=myfiles).execute() + + settings, trees, mtimedb = load_emerge_config() # once more, since pre-run might've done something portdb = trees[settings["ROOT"]]["porttree"].dbapi + + # Have post-run hooks executed whenever portage quits + portage.process.atexit_register(portage.hooks.HookDirectory(phase='post-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..ec4436d --- /dev/null +++ b/pym/portage/hooks.py @@ -0,0 +1,84 @@ +# Copyright 1998-2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# $Id$ + +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) diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py index 531cb23..9bb2b88 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, @@ -1048,6 +1051,8 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0, # If necessary, depend phase has been triggered by aux_get calls # and the exemption is no longer needed. portage._doebuild_manifest_exempt_depend -= 1 + + HookDirectory(phase='post-ebuild', settings=mysettings, myopts=None, myaction=mydo, mytargets=[mysettings["EBUILD"]]).execute() def _validate_deps(mysettings, myroot, mydo, mydbapi): 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