aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAvatar Piotr Jaroszyński <peper@gentoo.org> 2007-09-10 09:50:32 +0000
committerAvatar Piotr Jaroszyński <peper@gentoo.org> 2007-09-10 09:50:32 +0000
commit7f30d2d58c9649051616d0691d9e68ed6c823295 (patch)
treea2544d43a94894dfe868428c75117d2a79fd9a0c
parentf0b9aa0c2d9f8058ec8c73e961bfb14591ce04ee (diff)
downloadpaludis-7f30d2d58c9649051616d0691d9e68ed6c823295.tar.gz
paludis-7f30d2d58c9649051616d0691d9e68ed6c823295.tar.xz
Python Hooks.
-rw-r--r--configure.ac1
-rw-r--r--doc/hooks.html.skel44
-rw-r--r--paludis/Makefile.am.m416
-rw-r--r--paludis/hooker.cc104
-rw-r--r--paludis/hooker.hh15
-rw-r--r--paludis/hooker_TEST.cc25
-rwxr-xr-xpaludis/hooker_TEST_setup.sh25
-rw-r--r--paludis/python_hooks.cc436
-rw-r--r--python/Makefile.am2
-rw-r--r--python/paludis_output_wrapper.py102
10 files changed, 746 insertions, 24 deletions
diff --git a/configure.ac b/configure.ac
index 0c96fff..17c7834 100644
--- a/configure.ac
+++ b/configure.ac
@@ -885,6 +885,7 @@ int main() { boost::python::throw_error_already_set(); return 0; }
LIBS="$save_LIBS"
CXXFLAGS="$save_CXXFLAGS"
dnl }}}
+ AC_DEFINE([ENABLE_PYTHON], [1], [Enable Python])
fi
AM_CONDITIONAL([ENABLE_PYTHON], test "x$enable_python" = "xyes")
diff --git a/doc/hooks.html.skel b/doc/hooks.html.skel
index 430d22a..36b432f 100644
--- a/doc/hooks.html.skel
+++ b/doc/hooks.html.skel
@@ -39,7 +39,7 @@ that is executed when a particular well defined action occurs.</p>
See <a href="#merger-hooks">Merger / Unmerger Hooks</a>.</li>
</ul>
-<p>There are currently four categories of hook execution code:</p>
+<p>There are currently five categories of hook execution code:</p>
<ul>
<li><code>.bash</code> hooks. These are simple <code>.bash</code> files that
@@ -49,8 +49,11 @@ that is executed when a particular well defined action occurs.</p>
<li><code>.hook</code> hooks. These are also <code>bash</code> files, but rather
than containing the relevant code in global scope, they make use of functions
to perform hook actions. They also support specifying execution order dependencies
- upon other <code>.hook</code> files. See <a href="#hook-hooks">Hook Hooks</a>
- for details.</li>
+ upon other hooks. See <a href="#hook-hooks">Hook Hooks</a> for details.</li>
+
+ <li><code>.py</code> hooks. These are much like .hook hooks, but written in Python and
+ with additional power given by Python bindings and access to the current Environment.
+ See <a href="#py-hooks">Python Hooks</a> for details.</li>
<li><code>.so</code> hooks. These are written in C++ and compiled into shared
libraries, and run inside the Paludis process. See <a href="#so-hooks">So Hooks</a>
@@ -68,6 +71,7 @@ following table indicates availability:</p>
<td></td>
<th><code>.bash</code></th>
<th><code>.hook</code></th>
+ <th><code>.py</code></th>
<th><code>.so</code></th>
<th>Repository</th>
</tr>
@@ -77,6 +81,7 @@ following table indicates availability:</p>
<td>yes</td>
<td>yes</td>
<td>yes</td>
+ <td>yes</td>
</tr>
<tr>
<th>Ebuild Phase</th>
@@ -98,6 +103,7 @@ following table indicates availability:</p>
<td>yes</td>
<td>yes</td>
<td>yes</td>
+ <td>yes</td>
</tr>
</table>
@@ -369,6 +375,38 @@ hook_after_install_all_post()
are cached, and are generally only called once per session, so the output should
not vary based upon outside parameters.</p>
+<h4 id="py-hooks">Python Hooks</h4>
+
+<p>A <code>.py</code> hook is much like <code>.hook</code> hook, but written
+in Python and with full access to the current Paludis environment through
+Python bindings. For each hook it can handle it must, at minimum, define
+a function named <code>hook_run_$HOOK</code> which accepts exactly two positional
+arguments: the current Environment used by Paludis, and the additional Hook environment variables
+represented by a Python dictionary. It may also define the <code>hook_depend_$HOOK</code>
+and <code>hook_after_$HOOK</code> functions which must take exactly one argument,
+the Hook environment, and return a list of strings.
+For example:</p>
+
+<pre>
+def hook_run_install_all_post(env, hook_env):
+ from paludis import *
+
+ print "* Checking for monkeys..."
+
+ if list(env.package_database().query(Query.Package("nice/monkey"))):
+ print "Found a monkey!"
+ else:
+ print "No monkeys found"
+
+def hook_depend_install_all_post(hook_env):
+ # we need to run after the Paludis standard eselect_env_update hook
+ return ["eselect_env_update"]
+
+def hook_after_install_all_post(hook_env):
+ # if checking for rabbits or squirrels, do those first
+ return ["check_for_rabbits", "check_for_squirrels"]
+</pre>
+
<h4 id="so-hooks">So Hooks</h4>
<p>A <code>.so</code> hook is written in C++ and has full access to the Paludis public API.
diff --git a/paludis/Makefile.am.m4 b/paludis/Makefile.am.m4
index b8dcd71..685ab40 100644
--- a/paludis/Makefile.am.m4
+++ b/paludis/Makefile.am.m4
@@ -77,7 +77,8 @@ DEFS= \
-DSYSCONFDIR=\"$(sysconfdir)\" \
-DLIBEXECDIR=\"$(libexecdir)\" \
-DDATADIR=\"$(datadir)\" \
- -DLIBDIR=\"$(libdir)\"
+ -DLIBDIR=\"$(libdir)\" \
+ -DPYTHONINSTALLDIR=\"$(PYTHON_INSTALL_DIR)\"
EXTRA_DIST = about.hh.in Makefile.am.m4 paludis.hh.m4 files.m4 \
testscriptlist srlist srcleanlist selist secleanlist \
repository_blacklist.txt hooker.bash
@@ -87,6 +88,10 @@ BUILT_SOURCES = srcleanlist secleanlist
libpaludis_la_SOURCES = filelist
libpaludis_la_LDFLAGS = -version-info @VERSION_LIB_CURRENT@:@VERSION_LIB_REVISION@:0 $(PTHREAD_LIBS)
+libpaludispythonhooks_la_SOURCES = python_hooks.cc
+libpaludispythonhooks_la_CXXFLAGS = $(AM_CXXFLAGS) -I@PYTHON_INCLUDE_DIR@
+libpaludispythonhooks_la_LDFLAGS = -version-info @VERSION_LIB_CURRENT@:@VERSION_LIB_REVISION@:0 @BOOST_PYTHON_LIB@ -lpython@PYTHON_VERSION@
+
libpaludismanpagethings_la_SOURCES = name.cc
libpaludissohooks_TEST_la_SOURCES = sohooks_TEST.cc
@@ -125,8 +130,13 @@ else
lib_LTLIBRARIES = libpaludis.la
noinst_LTLIBRARIES = libpaludismanpagethings.la
+if ENABLE_PYTHON
+lib_LTLIBRARIES += libpaludispythonhooks.la
+endif
+
endif
+
paludis_includedir = $(includedir)/paludis-$(PALUDIS_PC_SLOT)/paludis/
paludis_include_HEADERS = headerlist srheaderlist seheaderlist
@@ -157,5 +167,9 @@ TESTS_ENVIRONMENT = env \
PALUDIS_REPOSITORY_SO_DIR="$(top_builddir)/paludis/repositories" \
TEST_SCRIPT_DIR="$(srcdir)/" \
SO_SUFFIX=@VERSION_LIB_CURRENT@ \
+ PYTHONPATH="$(top_builddir)/python/" \
+ PALUDIS_PYTHON_DIR="$(top_srcdir)/python/" \
+ LD_LIBRARY_PATH="`echo $$LD_LIBRARY_PATH: | sed -e 's,^:,,'`` \
+ $(top_srcdir)/paludis/repositories/e/ebuild/utils/canonicalise $(top_builddir)/paludis/.libs/`" \
bash $(top_srcdir)/test/run_test.sh
diff --git a/paludis/hooker.cc b/paludis/hooker.cc
index 90c5350..0f82ade 100644
--- a/paludis/hooker.cc
+++ b/paludis/hooker.cc
@@ -19,6 +19,7 @@
*/
#include "hooker.hh"
+#include "config.h"
#include <paludis/environment.hh>
#include <paludis/hook.hh>
#include <paludis/hashed_containers.hh>
@@ -30,7 +31,6 @@
#include <paludis/util/system.hh>
#include <paludis/util/private_implementation_pattern-impl.hh>
#include <paludis/util/strip.hh>
-#include <paludis/util/graph.hh>
#include <paludis/util/graph-impl.hh>
#include <paludis/util/pstream.hh>
#include <paludis/util/tokeniser.hh>
@@ -40,25 +40,17 @@
#include <iterator>
#include <dlfcn.h>
+#ifdef ENABLE_PYTHON
+# include <boost/version.hpp>
+# if BOOST_VERSION >= 103400
+# define PYTHON_HOOKS 1
+# endif
+#endif
+
using namespace paludis;
namespace
{
- class HookFile;
-
- class HookFile :
- private InstantiationPolicy<HookFile, instantiation_method::NonCopyableTag>
- {
- public:
- virtual ~HookFile()
- {
- }
-
- virtual HookResult run(const Hook &) const PALUDIS_ATTRIBUTE((warn_unused_result)) = 0;
- virtual const FSEntry file_name() const = 0;
- virtual void add_dependencies(const Hook &, DirectedGraph<std::string, int> &) = 0;
- };
-
class BashHookFile :
public HookFile
{
@@ -95,7 +87,7 @@ namespace
const bool _run_prefixed;
const Environment * const _env;
- virtual void _add_dependency_class(const Hook &, DirectedGraph<std::string, int> &, bool);
+ void _add_dependency_class(const Hook &, DirectedGraph<std::string, int> &, bool);
public:
FancyHookFile(const FSEntry & f, const bool r, const Environment * const e) :
@@ -390,6 +382,31 @@ Hooker::add_dir(const FSEntry & dir, const bool v)
_imp->dirs.push_back(std::make_pair(dir, v));
}
+namespace
+{
+ struct PyHookFileHandle
+ {
+ Mutex mutex;
+ void * handle;
+ tr1::shared_ptr<HookFile> (* create_py_hook_file_handle)(const FSEntry &,
+ const bool, const Environment * const);
+
+
+ PyHookFileHandle() :
+ handle(0),
+ create_py_hook_file_handle(0)
+ {
+ }
+
+ ~PyHookFileHandle()
+ {
+ if (0 != handle)
+ dlclose(handle);
+ }
+
+ } pyhookfilehandle;
+}
+
HookResult
Hooker::perform_hook(const Hook & hook) const
{
@@ -482,6 +499,59 @@ Hooker::perform_hook(const Hook & hook) const
+ "' because of naming conflict with '" + stringify(
hook_files.find(stringify(strip_trailing_string(e->basename(), so_suffix)))->second->file_name()) + "'");
+#ifdef PYTHON_HOOKS
+ if (is_file_with_extension(*e, ".py", IsFileWithOptions()))
+ {
+ static bool load_try(false);
+ static bool load_ok(false);
+
+ {
+ Lock lock(pyhookfilehandle.mutex);
+
+ if (! load_try)
+ {
+ load_try = true;
+
+ pyhookfilehandle.handle = dlopen("libpaludispythonhooks.so", RTLD_NOW | RTLD_GLOBAL);
+ if (pyhookfilehandle.handle)
+ {
+ pyhookfilehandle.create_py_hook_file_handle =
+ reinterpret_cast<tr1::shared_ptr<HookFile> (*)(
+ const FSEntry &, const bool, const Environment * const)>(
+ reinterpret_cast<uintptr_t>(dlsym(
+ pyhookfilehandle.handle, "create_py_hook_file")));
+ if (pyhookfilehandle.create_py_hook_file_handle)
+ {
+ load_ok = true;
+ }
+ else
+ {
+ Log::get_instance()->message(ll_warning, lc_context,
+ "dlsym(libpaludispythonhooks.so, create_py_hook_file) "
+ "failed due to error '" + stringify(dlerror()) + "'");
+ }
+ }
+ else
+ {
+ Log::get_instance()->message(ll_warning, lc_context,
+ "dlopen(libpaludispythonhooks.so) "
+ "failed due to error '" + stringify(dlerror()) + "'");
+ }
+ }
+ }
+ if (load_ok)
+ {
+ if (! hook_files.insert(std::make_pair(strip_trailing_string(e->basename(), ".py"),
+ tr1::shared_ptr<HookFile>(pyhookfilehandle.create_py_hook_file_handle(
+ *e, d->second, _imp->env)))).second)
+ Log::get_instance()->message(ll_warning, lc_context,
+ "Discarding hook file '" + stringify(*e)
+ + "' because of naming conflict with '"
+ + stringify(hook_files.find(stringify(strip_trailing_string(
+ e->basename(), ".py")))->second->file_name()) + "'");
+ }
+ }
+#endif
}
}
diff --git a/paludis/hooker.hh b/paludis/hooker.hh
index 2668ec8..1791aa8 100644
--- a/paludis/hooker.hh
+++ b/paludis/hooker.hh
@@ -22,6 +22,8 @@
#include <paludis/util/instantiation_policy.hh>
#include <paludis/util/private_implementation_pattern.hh>
+#include <paludis/util/graph-fwd.hh>
+#include <string>
namespace paludis
{
@@ -30,6 +32,19 @@ namespace paludis
class Hook;
class HookResult;
+ class HookFile :
+ private InstantiationPolicy<HookFile, instantiation_method::NonCopyableTag>
+ {
+ public:
+ virtual ~HookFile()
+ {
+ }
+
+ virtual HookResult run(const Hook &) const PALUDIS_ATTRIBUTE((warn_unused_result)) = 0;
+ virtual const FSEntry file_name() const = 0;
+ virtual void add_dependencies(const Hook &, DirectedGraph<std::string, int> &) = 0;
+ };
+
/**
* Handles executing hooks for an Environment.
*
diff --git a/paludis/hooker_TEST.cc b/paludis/hooker_TEST.cc
index 429c46c..714a276 100644
--- a/paludis/hooker_TEST.cc
+++ b/paludis/hooker_TEST.cc
@@ -17,6 +17,7 @@
* Place, Suite 330, Boston, MA 02111-1307 USA
*/
+#include "config.h"
#include <paludis/hooker.hh>
#include <paludis/hook.hh>
#include <paludis/environments/test/test_environment.hh>
@@ -26,6 +27,13 @@
#include <fstream>
#include <iterator>
+#ifdef ENABLE_PYTHON
+# include <boost/version.hpp>
+# if BOOST_VERSION >= 103400
+# define PYTHON_HOOKS 1
+# endif
+#endif
+
using namespace test;
using namespace paludis;
@@ -42,6 +50,10 @@ namespace test_cases
HookResult result(0, "");
hooker.add_dir(FSEntry("hooker_TEST_dir/"), false);
+#ifdef PYTHON_HOOKS
+ result = hooker.perform_hook(Hook("py_hook"));
+ TEST_CHECK_EQUAL(result.max_exit_status, 0);
+#endif
result = hooker.perform_hook(Hook("simple_hook"));
TEST_CHECK_EQUAL(result.max_exit_status, 3);
TEST_CHECK_EQUAL(result.output, "");
@@ -77,7 +89,12 @@ namespace test_cases
std::ifstream f(stringify(FSEntry("hooker_TEST_dir/ordering.out")).c_str());
std::string line((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
+#ifdef PYTHON_HOOKS
+ TEST_CHECK_EQUAL(line, "e\nc\nf\nd\nb\na\npy_hook\ng\ni\nh\nsohook\nk\nj\n");
+#else
TEST_CHECK_EQUAL(line, "e\nc\nf\nd\nb\na\ng\ni\nh\nsohook\nk\nj\n");
+#endif
+
}
} test_hooker_ordering;
@@ -156,6 +173,13 @@ namespace test_cases
TEST_CHECK_EQUAL(result.max_exit_status, 0);
TEST_CHECK_EQUAL(result.output, "foo");
+#ifdef PYTHON_HOOKS
+ result = hooker.perform_hook(Hook("py_hook_output")
+ .grab_output(Hook::AllowedOutputValues()("foo")));
+ TEST_CHECK_EQUAL(result.max_exit_status, 0);
+ TEST_CHECK_EQUAL(result.output, "foo");
+#endif
+
result = hooker.perform_hook(Hook("several_hooks_output")
.grab_output(Hook::AllowedOutputValues()));
TEST_CHECK_EQUAL(result.max_exit_status, 0);
@@ -208,6 +232,5 @@ namespace test_cases
TEST_CHECK_EQUAL(line, "one\ntwo\nthree\n");
}
} test_hooker_bad_output_hooks;
-
}
diff --git a/paludis/hooker_TEST_setup.sh b/paludis/hooker_TEST_setup.sh
index ec5338d..19c382c 100755
--- a/paludis/hooker_TEST_setup.sh
+++ b/paludis/hooker_TEST_setup.sh
@@ -56,6 +56,18 @@ ln -s ../../.libs/libpaludissohooks_TEST.so.${SO_SUFFIX} so_hook
mkdir so_hook_output
ln -s ../../.libs/libpaludissohooks_TEST.so.${SO_SUFFIX} so_hook_output
+mkdir py_hook
+
+cat <<"END" > py_hook/hook.py
+def hook_run_py_hook(env, hook_env):
+ pass
+END
+
+mkdir py_hook_output
+cat <<"END" > py_hook_output/hook.py
+def hook_run_py_hook_output(env, hook_env):
+ return "foo"
+END
mkdir several_hooks
cat <<"END" > several_hooks/one.hook
@@ -198,7 +210,7 @@ hook_after_ordering() {
echo x
;;
h)
- echo i
+ echo i py_hook
;;
j)
echo k libpaludissohooks_TEST
@@ -212,6 +224,12 @@ for a in a b c d e f g h i j k ; do
ln -s ../ordering.common ordering/${a}.hook
done
ln -s ../../.libs/libpaludissohooks_TEST.so.${SO_SUFFIX} ordering
+cat <<"END" > ordering/py_hook.py
+def hook_run_ordering(env, hook_env):
+ file("hooker_TEST_dir/ordering.out", "a").write("py_hook\n")
+def hook_depend_ordering(hook_env):
+ return ["f"]
+END
mkdir bad_hooks
cat <<"END" > bad_hooks.common
@@ -230,6 +248,11 @@ asdf
END
chmod +x bad_hooks/two.hook
+cat <<"END" > bad_hooks/four.py
+def hook_run_bad_hooks(env, hook_env):
+ 1/0
+END
+
mkdir cycles
cat <<"END" > cycles.common
hook_run_cycles() {
diff --git a/paludis/python_hooks.cc b/paludis/python_hooks.cc
new file mode 100644
index 0000000..be68e43
--- /dev/null
+++ b/paludis/python_hooks.cc
@@ -0,0 +1,436 @@
+/* vim: set sw=4 sts=4 et foldmethod=syntax : */
+
+#include <boost/python.hpp>
+
+#include <paludis/hooker.hh>
+#include <paludis/hook.hh>
+#include <paludis/environment.hh>
+#include <paludis/util/fs_entry.hh>
+#include <paludis/util/graph-impl.hh>
+#include <paludis/util/log.hh>
+#include <paludis/util/stringify.hh>
+#include <paludis/util/strip.hh>
+#include <paludis/util/system.hh>
+#include <paludis/util/mutex.hh>
+
+#include <set>
+
+using namespace paludis;
+namespace bp = boost::python;
+
+extern "C"
+{
+ tr1::shared_ptr<HookFile> create_py_hook_file(const FSEntry &, const bool, const Environment * const)
+ PALUDIS_VISIBLE;
+}
+
+namespace
+{
+ class PyHookFile :
+ public HookFile
+ {
+ private:
+ static Mutex _mutex;
+
+ static bp::dict _local_namespace_base;
+ static bp::dict _output_wrapper_namespace;
+ static bp::object _format_exception;
+
+ const FSEntry _file_name;
+ const Environment * const _env;
+ const bool _run_prefixed;
+ bool _loaded;
+ bp::dict _local_namespace;
+
+ friend class Prefix;
+
+ class Prefix
+ {
+ private:
+ const PyHookFile * const phf;
+
+ public:
+ Prefix(const PyHookFile * const, const std::string &);
+ ~Prefix();
+ };
+
+ std::string _get_traceback() const;
+
+ void _add_dependency_class(const Hook &, DirectedGraph<std::string, int> &, bool);
+
+ public:
+ PyHookFile(const FSEntry &, const bool, const Environment * const);
+
+ virtual HookResult run(const Hook &) const PALUDIS_ATTRIBUTE((warn_unused_result));
+
+ virtual const FSEntry file_name() const
+ {
+ return _file_name;
+ }
+
+ virtual void add_dependencies(const Hook &, DirectedGraph<std::string, int> &);
+ };
+ Mutex PyHookFile::_mutex;
+ bp::dict PyHookFile::_output_wrapper_namespace;
+ bp::dict PyHookFile::_local_namespace_base;
+ bp::object PyHookFile::_format_exception;
+}
+
+PyHookFile::PyHookFile(const FSEntry & f, const bool r, const Environment * const e) :
+ _file_name(f),
+ _env(e),
+ _run_prefixed(r),
+ _loaded(false)
+{
+ Lock l(_mutex);
+
+ static bool initialized(false);
+
+ if (! initialized)
+ {
+ initialized = true;
+ try
+ {
+ Py_Initialize();
+
+ bp::object main = bp::import("__main__");
+ bp::object global_namespace = main.attr("__dict__");
+
+ _local_namespace_base["__builtins__"] = global_namespace["__builtins__"];
+ _output_wrapper_namespace = _local_namespace_base.copy();
+
+ bp::object traceback = bp::import("traceback");
+ bp::object traceback_namespace = traceback.attr("__dict__");
+ _format_exception = traceback_namespace["format_exception"];
+
+ bp::exec_file(
+ (getenv_with_default("PALUDIS_PYTHON_DIR", PYTHONINSTALLDIR)
+ + "/paludis_output_wrapper.py").c_str(),
+ _output_wrapper_namespace, _output_wrapper_namespace);
+ bp::import("paludis");
+ }
+ catch (const bp::error_already_set &)
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context, "Initializing Python interpreter failed:");
+ PyErr_Print();
+ return;
+ }
+ catch (const std::exception & ex)
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ std::string("Initializing Python interpreter failed: '") + ex.what() + "'");
+ return;
+ }
+ }
+ _local_namespace = _local_namespace_base.copy();
+
+ try
+ {
+ bp::exec_file(stringify(f).c_str(), _local_namespace, _local_namespace);
+ _loaded = true;
+ }
+ catch (const bp::error_already_set &)
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context, "Loading hook '" + stringify(f) + "' failed: '"
+ + _get_traceback() + "'");
+ }
+ catch (const std::exception & ex)
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context, "Loading hook '" + stringify(f) + "' failed: '"
+ + ex.what() + "'");
+ }
+}
+
+HookResult
+PyHookFile::run(const Hook & hook) const
+{
+ Context c("When running hook '" + stringify(file_name()) + "' for hook '" + hook.name() + "':");
+
+ if (! _loaded)
+ return HookResult(0, "");
+
+ Lock l(_mutex);
+
+ bp::object _run;
+
+ try
+ {
+ _run = _local_namespace["hook_run_" + hook.name()];
+ }
+ catch (const bp::error_already_set &)
+ {
+ if (PyErr_ExceptionMatches(PyExc_KeyError))
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ "Hook '" + stringify(file_name()) + "' does not define the hook_run_"
+ + hook.name() + " function");
+ PyErr_Clear();
+ }
+ else
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ "Hook '" + stringify(file_name()) + "' failed unexpectedly: '"
+ + _get_traceback() + "'");
+ }
+
+ return HookResult(1, "");
+ }
+
+ Prefix p(this, _run_prefixed ? strip_trailing_string(file_name().basename(), ".py") + "> " : "");
+
+ Log::get_instance()->message(ll_debug, lc_no_context, "Starting hook '" +
+ stringify(file_name()) + "' for '" + hook.name() + "'");
+
+ bp::dict hook_env;
+ hook_env["HOOK"] = hook.name();
+ hook_env["HOOK_FILE"] = stringify(file_name());
+
+ for (Hook::Iterator x(hook.begin()), x_end(hook.end()) ; x != x_end ; ++x)
+ hook_env[x->first] = x->second;
+
+ bp::object result;
+ try
+ {
+ result = _run(bp::ptr(_env), hook_env);
+ }
+ catch (const bp::error_already_set &)
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ "Hook '" + stringify(file_name()) + "': running hook_run_" + hook.name()
+ + " function failed: '" + _get_traceback() + "'");
+ return HookResult(1, "");
+ }
+
+ if (hook.output_dest == hod_grab)
+ {
+ if (bp::extract<std::string>(result).check())
+ {
+ std::string result_s = bp::extract<std::string>(result);
+
+ Log::get_instance()->message(ll_debug, lc_no_context,
+ "Hook '" + stringify(file_name()) + "': hook_run_" + hook.name()
+ + " function returned '" + result_s + "'");
+
+ return HookResult(0, result_s);
+ }
+ else
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ "Hook '" + stringify(file_name()) + "': hook_run_" + hook.name()
+ + " function returned not a string.");
+ return HookResult(1, "");
+ }
+ }
+ else
+ return HookResult(0, "");
+}
+
+void
+PyHookFile::add_dependencies(const Hook & hook, DirectedGraph<std::string, int> & g)
+{
+ Context c("When finding dependencies of hook script '" + stringify(file_name())
+ + "' for hook '" + hook.name() + "':");
+
+ if (! _loaded)
+ return;
+
+ Lock l(_mutex);
+
+ Prefix p(this, _run_prefixed ? strip_trailing_string(file_name().basename(), ".py") + "> " : "");
+
+ _add_dependency_class(hook, g, false);
+ _add_dependency_class(hook, g, true);
+}
+
+void
+PyHookFile::_add_dependency_class(const Hook & hook, DirectedGraph<std::string, int> & g, bool depend)
+{
+ Context context("When adding dependency class '" + stringify(depend ? "depend" : "after") + "' for hook '"
+ + stringify(hook.name()) + "' file '" + stringify(file_name()) + "':");
+
+ bp::object _run;
+ try
+ {
+ _run = _local_namespace["hook_" + stringify(depend ? "depend" : "after") + "_" + hook.name()];
+ }
+ catch (const bp::error_already_set &)
+ {
+ if (PyErr_ExceptionMatches(PyExc_KeyError))
+ {
+ Log::get_instance()->message(ll_debug, lc_no_context,
+ "Hook '" + stringify(file_name()) + "' does not define the hook_"
+ + stringify(depend ? "depend" : "after") + "_" + hook.name() + " function");
+ PyErr_Clear();
+ }
+ else
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ "Hook '" + stringify(file_name()) + "' failed unexpectedly: '"
+ + _get_traceback() + "'");
+ }
+
+ return;
+ }
+
+ bp::dict hook_env;
+ hook_env["HOOK"] = hook.name();
+ hook_env["HOOK_FILE"] = stringify(file_name());
+
+ for (Hook::Iterator x(hook.begin()), x_end(hook.end()) ; x != x_end ; ++x)
+ hook_env[x->first] = x->second;
+
+ bp::object result;
+ try
+ {
+ result = _run(hook_env);
+ }
+ catch (const bp::error_already_set &)
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ "Hook '" + stringify(file_name()) + "': running hook_"
+ + stringify(depend ? "depend" : "after") + "_" + hook.name() + " function failed: '"
+ + _get_traceback() + "'");
+ return;
+ }
+
+ bp::str py_deps(result);
+ std::string deps = bp::extract<std::string>(py_deps);
+
+ std::set<std::string> deps_s;
+
+ if (bp::extract<bp::list>(result).check())
+ {
+ bp::list l = bp::extract<bp::list>(result);
+
+ while (PyList_Size(l.ptr()))
+ {
+ bp::object o(l.pop());
+ if (bp::extract<std::string>(o).check())
+ {
+ deps_s.insert(bp::extract<std::string>(o));
+ }
+ else
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ "Hook '" + stringify(file_name()) + "': hook_"
+ + stringify(depend ? "depend" : "after") + "_" + hook.name()
+ + " function returned not a list of strings: '" + deps + "'");
+ return;
+ }
+ }
+ }
+ else
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ "Hook '" + stringify(file_name()) + "': hook_"
+ + stringify(depend ? "depend" : "after") + "_" + hook.name()
+ + " function returned not a list: '" + deps + "'");
+ return;
+ }
+
+ Log::get_instance()->message(ll_debug, lc_no_context,
+ "Hook '" + stringify(file_name()) + "': hook_"
+ + stringify(depend ? "depend" : "after") + "_" + hook.name() + " function returned '" + deps + "'");
+
+ for (std::set<std::string>::const_iterator d(deps_s.begin()), d_end(deps_s.end()) ;
+ d != d_end ; ++d)
+ {
+ if (g.has_node(*d))
+ g.add_edge(strip_trailing_string(file_name().basename(), ".py"), *d, 0);
+ else if (depend)
+ Log::get_instance()->message(ll_warning, lc_context, "Hook dependency '" + stringify(*d) +
+ "' for '" + stringify(file_name()) + "' not found");
+ else
+ Log::get_instance()->message(ll_debug, lc_context, "Hook after '" + stringify(*d) +
+ "' for '" + stringify(file_name()) + "' not found");
+ }
+}
+
+PyHookFile::Prefix::Prefix(const PyHookFile * const f, const std::string & prefix):
+ phf(f)
+{
+ try
+ {
+ phf->_output_wrapper_namespace["set_prefix"](prefix);
+ }
+ catch (const bp::error_already_set &)
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ "Hook '" + stringify(phf->file_name()) + "' failed unexpectedly: '"
+ + phf->_get_traceback() + "'");
+ }
+}
+
+PyHookFile::Prefix::~Prefix()
+{
+ try
+ {
+ phf->_output_wrapper_namespace["restore_prefix"]();
+ }
+ catch (const bp::error_already_set &)
+ {
+ Log::get_instance()->message(ll_warning, lc_no_context,
+ "Hook '" + stringify(phf->file_name()) + "' failed unexpectedly: '"
+ + phf->_get_traceback() + "'");
+ }
+}
+
+std::string
+PyHookFile::_get_traceback() const
+{
+ if (! PyErr_Occurred())
+ return "";
+
+ Context c("When getting traceback");
+
+ PyObject * ptype, * pvalue, * ptraceback;
+ PyErr_Fetch(&ptype, &pvalue, &ptraceback);
+
+ if (ptype == NULL)
+ ptype = Py_None;
+ if (pvalue == NULL)
+ pvalue = Py_None;
+ if (ptraceback == NULL)
+ ptraceback = Py_None;
+
+ PyObject * result(PyObject_CallFunctionObjArgs(_format_exception.ptr(), ptype, pvalue, ptraceback, NULL));
+
+ if (result == NULL)
+ {
+ Log::get_instance()->message(ll_warning, lc_context,
+ "Hook '" + stringify(file_name()) + "': _get_traceback(): traceback.format_exception failed");
+ return "Getting traceback failed";
+ }
+
+ bp::list l;
+ if (bp::extract<bp::list>(result).check())
+ {
+ l = bp::extract<bp::list>(result);
+ }
+ else
+ {
+ Log::get_instance()->message(ll_warning, lc_context,
+ "Hook '" + stringify(file_name()) + "': _get_traceback(): cannot extract list of lines");
+ return "Getting traceback failed";
+ }
+
+ bp::str result_str;
+ try
+ {
+ result_str = result_str.join(l);
+ }
+ catch (const bp::error_already_set &)
+ {
+ Log::get_instance()->message(ll_warning, lc_context,
+ "Hook '" + stringify(file_name()) + "': _get_traceback(): joining list of lines failed");
+ return "Getting traceback failed";
+ }
+
+ return strip_trailing(bp::extract<std::string>(result_str)(), "\n");
+}
+
+tr1::shared_ptr<HookFile>
+create_py_hook_file(const FSEntry & f, const bool b, const Environment * const e)
+{
+ return tr1::shared_ptr<HookFile>(new PyHookFile(f, b, e));
+}
diff --git a/python/Makefile.am b/python/Makefile.am
index b6aba3e..45b1d12 100644
--- a/python/Makefile.am
+++ b/python/Makefile.am
@@ -167,7 +167,7 @@ libadditionaltests_la_LIBADD = $(libpaludispython_la_LIBADD)
check_DATA = .libs/paludis.so .libs/additional_tests.so
pythonlibdir = @PYTHON_INSTALL_DIR@
-pythonlib_DATA = paludis.so
+pythonlib_DATA = paludis.so paludis_output_wrapper.py
.libs/paludis.so : libpaludispython.la paludis_python_so.o
mkdir -p .libs
diff --git a/python/paludis_output_wrapper.py b/python/paludis_output_wrapper.py
new file mode 100644
index 0000000..66dee76
--- /dev/null
+++ b/python/paludis_output_wrapper.py
@@ -0,0 +1,102 @@
+# vim: set fileencoding=utf-8 sw=4 sts=4 et :
+
+#
+# Copyright (c) 2007 Piotr Jaroszyński <peper@gentoo.org>
+#
+# This file is part of the Paludis package manager. Paludis is free software;
+# you can redistribute it and/or modify it under the terms of the GNU General
+# Public License version 2, as published by the Free Software Foundation.
+#
+# Paludis is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+# Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+
+stdout_cache = []
+stderr_cache = []
+
+class CommonOutput:
+ def __init__(self, out):
+ self.out = out
+
+ def writelines(self, seq):
+ for s in seq:
+ self.write(s)
+
+ def closed(self):
+ self.out.closed()
+
+ def close(self):
+ self.out.close()
+
+ def isatty(self):
+ return self.out.isatty()
+
+ def mode(self):
+ return self.out.mode()
+
+ def name(self):
+ return self.out.name()
+
+ def encoding(self):
+ return self.out.encoding()
+
+class PrefixOutput(CommonOutput):
+ def __init__(self, out, prefix):
+ CommonOutput.__init__(self, out)
+
+ self.prefix = prefix
+ self.need_prefix = True
+
+ def write(self, str):
+ if self.need_prefix:
+ self.out.write(self.prefix)
+
+ if str.endswith("\n"):
+ self.need_prefix = True
+ newlines = -1
+ else:
+ self.need_prefix = False
+ newlines = 0
+
+ newlines += str.count("\n")
+ self.out.write(str.replace("\n", "\n" + self.prefix, newlines))
+
+def save(what):
+ if what == "stderr" or what == "both":
+ stderr_cache.append(sys.stderr)
+ elif what == "stdout" or what == "both":
+ stdout_cache.append(sys.stdout)
+
+def restore(what):
+ if what == "stderr" or what == "both":
+ if len(stderr_cache):
+ sys.stderr = stderr_cache.pop()
+ else:
+ sys.stderr = sys.__stderr__
+
+ elif what == "stdout" or what == "both":
+ if len(stdout_cache):
+ sys.stdout = stdout_cache.pop()
+ else:
+ sys.stdout = sys.__stdout__
+
+def set_prefix(prefix):
+ save("both")
+ if prefix:
+ sys.stdout = PrefixOutput(sys.__stdout__, prefix)
+ sys.stderr = PrefixOutput(sys.__stderr__, prefix)
+ else:
+ sys.stdout = sys.__stdout__
+ sys.stderr = sys.__stderr__
+
+def restore_prefix():
+ restore("both")
+