SCons:如何在某些目标构建后生成依赖?
我有一个使用SCons的项目,主要是构建一些Python模块(大部分是从C++编译的共享对象)。这一部分运行得很好,所有的依赖关系似乎都没问题。
不过,现在我到了需要为这些模块添加测试的阶段。这些测试应该作为构建的一部分来运行。测试是用Python写的,运行时需要在一个已经构建好所有模块的环境下进行(这样import
语句才能找到它们)。
我把这一部分搞定了,测试可以运行,但我找不到办法从import
语句生成它们的依赖关系。
我发现了一个叫做modulefinder的工具,它正好可以满足我的需求。而且我可以在构建好的环境中运行它,并在我的项目构建之后得到预期的结果。我想在emitter中使用modulefinder
来扫描测试/Python脚本所依赖的文件。
问题是,依赖扫描/构建和运行发射器的过程发生在SCons构建所有模块之前,这样就无法为测试正确设置环境,因此modulefinder
也无法工作。
我还没找到办法让SCons在某些目标已经构建后,去查找特定目标的依赖关系。
补充:
我在SCons文档中找到了ParseDepends,它似乎也在讨论类似的问题(几乎是完全相同,只是语言不同)。
ParseDepends的这个限制会导致不必要的重新编译。因此,只有在没有可用的扫描器或扫描器对特定任务不够强大时,才应该使用ParseDepends。
不过我仍然抱有希望,希望能找到一个干净的解决方案来解决我的问题。
2 个回答
在编译阶段,你不能在SCons中更改依赖关系。SCons会创建一个依赖树,并在运行后就不能再更改了。
我建议你为你的构建器写一个扫描器。在C++中,SCons使用扫描器来查找包含的依赖关系。
经过一番尝试,我找到了一种不太干净但也还不错的方法,似乎可以正常工作。我把它封装在一个继承自 Scanner
的辅助类里:
from SCons.Node import Node
from SCons import Scanner
import logging
_LOGGER = logging.getLogger(__name__)
class DeferredScanner(Scanner.Current):
"""
This is a helper class for implementing source scanners that need
to wait for specific things to be built before source scanning can happen.
One practical example of usage is when you are you generating Python
modules (i.e. shared libraries) which you want to test.
You have to wait for all your modules are ready before dependencies
of your tests can be scanned.
To do this approach with this scanner is to collect all generated modules
and `wait_for` them before scanning dependncies of whatever this scanner
is used for.
Sample usage:
py_scanner = DeferredScanner(
wait_for = all_generated_modules,
function = _python_source_scanner,
recursive = True,
skeys = ['.py'],
path_function = FindENVPathDirs('PYTHONPATH'),
)
"""
def __init__(self, wait_for, **kw):
Scanner.Current.__init__(
self,
node_factory = Node,
**kw
)
self.wait_for = wait_for
self.sources_to_rescan = []
self.ready = False
env = wait_for[0].get_build_env()
env.AlwaysBuild(wait_for)
self._hook_implicit_reset(
wait_for[0],
'built',
self.sources_to_rescan,
lambda: setattr(self, 'ready', True),
)
def _hook_implicit_reset(self, node, name, on, extra = None):
# We can only modify dependencies in main thread
# of Taskmaster processing.
# However there seems to be no hook to "sign up" for
# callback when post processing of built node is hapenning
# (at least I couldn't find anything like this in SCons
# 2.2.0).
# AddPostAction executes actions in Executor thread, not
# main one, se we can't used that.
#
# `built` is guaranteed to be executed in that thread,
# so we hijack this one.
#
node.built = lambda old=node.built: (
old(),
self._reset_stored_dependencies(name, on),
extra and extra(),
)[0]
def _reset_stored_dependencies(self, name, on):
_LOGGER.debug('Resetting dependencies for {0}-{1} files: {2}'.format(
# oh, but it does have those
self.skeys, # pylint: disable=no-member
name,
len(on),
))
for s in on:
# I can't find any official way to invalidate
# FS.File (or Node in general) list of implicit dependencies
# that were found.
# Yet we want to force that Node to scan its implicit
# dependencies again.
# This seems to do the trick.
s._memo.pop('get_found_includes', None) # pylint: disable=protected-access
def __call__(self, node, env, path = ()):
ready = self.ready
_LOGGER.debug('Attempt to scan {0} {1}'.format(str(node), ready))
deps = self.wait_for + [node]
if ready:
deps.extend(Scanner.Current.__call__(self, node, env, path))
self.sources_to_rescan.append(node)
# In case `wait_for` is not dependent on this node
# we have to make sure we will rescan dependencies when
# this node is built itself.
# It boggles my mind that SCons scanns nodes before
# they exist, and caches result even if there was no
# list returned.
self._hook_implicit_reset(
node,
'self',
[node],
)
return deps
这个方法的效果正如我所期望的那样,能够完成任务。它的效率可能也算是不错的。
另外,我还想提一下,这个方法在 SCons
2.2.0 版本上可以正常使用,不过我猜在更新的版本上也不会有太大差别。