使用distutils编译时禁用输出

11
我有一个setup.py脚本,需要探测编译器是否支持TR1、是否存在windows.h(以添加NOMINMAX定义)等。我通过创建一个简单的程序,并尝试使用Distutils的Compiler类编译它来进行这些检查。有/没有错误就是我的答案。
这很有效,但意味着编译器的丑陋错误消息会打印到控制台上。有没有办法在手动调用compile函数时抑制错误消息?
以下是我的函数,它现在通过将错误流导向文件(回答了自己的问题)来消除错误消息:
def see_if_compiles(program, include_dirs, define_macros):
    """ Try to compile the passed in program and report if it compiles successfully or not. """
    from distutils.ccompiler import new_compiler, CompileError
    from shutil import rmtree
    import tempfile
    import os

    try:
        tmpdir = tempfile.mkdtemp()
    except AttributeError:
        # Python 2.2 doesn't have mkdtemp().
        tmpdir = "compile_check_tempdir"
        try:
            os.mkdir(tmpdir)
        except OSError:
            print "Can't create temporary directory. Aborting."
            sys.exit()

    old = os.getcwd()

    os.chdir(tmpdir)

    # Write the program
    f = open('compiletest.cpp', 'w')
    f.write(program)
    f.close()

    # redirect the error stream to keep ugly compiler error messages off the command line
    devnull = open('errors.txt', 'w')
    oldstderr = os.dup(sys.stderr.fileno())
    os.dup2(devnull.fileno(), sys.stderr.fileno())
    #
    try:
        c = new_compiler()
        for macro in define_macros:
            c.define_macro(name=macro[0], value=macro[1])
        c.compile([f.name], include_dirs=include_dirs)
        success = True
    except CompileError:
        success = False
    # undo the error stream redirect
    os.dup2(oldstderr, sys.stderr.fileno())
    devnull.close()

    os.chdir(old)
    rmtree(tmpdir)
    return success

这里有一个函数,它使用上述方法来检查头部是否存在。
def check_for_header(header, include_dirs, define_macros):
    """Check for the existence of a header file by creating a small program which includes it and see if it compiles."""
    program = "#include <%s>\n" % header
    sys.stdout.write("Checking for <%s>... " % header)
    success = see_if_compiles(program, include_dirs, define_macros)
    if (success):
        sys.stdout.write("OK\n");
    else:
        sys.stdout.write("Not found\n");
    return success

2
scipy教程http://docs.scipy.org/doc/scipy/reference/tutorial/weave.html#more-with-printf提出了同样的问题。如果你解决了,请确保更新它。 - agf
我认为这正是autoconf/automake的用武之地。 - George
@George,autoconf不是Python Way™。它也仅限于类Unix系统。Python模块通常有一个setup.py脚本,使用distutils来构建/安装自身,因此可以在任何Python可用的地方工作。此外,Python对编译器和构建选项非常挑剔,例如只能使用与Python一起构建的相同编译器。Distutils会为您在扩展被安装的任何系统上执行正确的操作。 - Adam
@agf,我找到了一种使用os.dup2进行流重定向的方法,它似乎有效。我还没有在Windows上尝试过,但我认为它也应该可以工作。 - Adam
5个回答

5

Zac的评论促使我进一步调查,我发现Mercurial的setup.py脚本有一个有效的方法来实现他的方法。你不能仅仅分配流,因为更改不会被编译器进程继承,但显然Python有我们的好朋友dup2()的形式,即os.dup2()。它允许相同的操作系统级别的流操作,这些操作都是我们所熟悉和喜爱的,并且可以被子进程继承。

Mercurial的函数重定向到/dev/null,但为了保持Windows兼容性,我只是将其重定向到文件,然后删除它。

Mercurial说:

# simplified version of distutils.ccompiler.CCompiler.has_function
# that actually removes its temporary files.
def hasfunction(cc, funcname):
    tmpdir = tempfile.mkdtemp(prefix='hg-install-')
    devnull = oldstderr = None
    try:
        try:
            fname = os.path.join(tmpdir, 'funcname.c')
            f = open(fname, 'w')
            f.write('int main(void) {\n')
            f.write('    %s();\n' % funcname)
            f.write('}\n')
            f.close()
            # Redirect stderr to /dev/null to hide any error messages
            # from the compiler.
            # This will have to be changed if we ever have to check
            # for a function on Windows.
            devnull = open('/dev/null', 'w')
            oldstderr = os.dup(sys.stderr.fileno())
            os.dup2(devnull.fileno(), sys.stderr.fileno())
            objects = cc.compile([fname], output_dir=tmpdir)
            cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
        except:
            return False
        return True
    finally:
        if oldstderr is not None:
            os.dup2(oldstderr, sys.stderr.fileno())
        if devnull is not None:
            devnull.close()
        shutil.rmtree(tmpdir)

好的。当我有投票权时,我会点赞的。谢谢你提醒我。你用的是哪个版本的Python -- 你真的需要嵌套的try吗?你可以使用try: stuff in try except: return False else: return True finally: stuff in finally吗?还是这只是在mercurial版本中,并且你只是没有更改它? - agf
那段代码直接来自Mercurial的源代码,不是我自己写的。请注意,它们重定向到/dev/null,这在Windows上会出问题。我将在问题中更新我的函数。 - Adam
我站在相反的立场上,希望将这些编译器错误重定向到日志框架。这个答案至少让我可以做到这一点(不是将stderr重定向到/dev/null,而是将其重定向到实际文件,然后在完成后读取它),但我不太喜欢这样做。我想知道是否只需调用类似于subprocess.Popen的东西并手动调用编译器,因为我相当确定我可以更轻松地捕获(或丢弃)stdout/stderr,并且随着我尝试使用它,ccompiler看起来越来越没用。 - searchengine27
@searchengine27,你可以使用Python的管道系统创建一个管道,然后使用os.dup2将编译器输出重定向到管道。然后你只需要读取管道即可。 - Adam
那本来是个更好的想法,但当我尝试 import pipes 时,我的Python解释器崩溃了。不过这是另一个问题。 - searchengine27

3
这是我最近写的一个有用的上下文管理器,因为我在处理pymssql时遇到了和distutils.ccompiler.CCompiler.has_function相同的问题。我原本想使用你的方法(很好,谢谢分享!),但后来我想如果我使用上下文管理器,可以用更少的代码实现,而且更加普适和灵活。这是我想出来的代码:
import contextlib


@contextlib.contextmanager
def stdchannel_redirected(stdchannel, dest_filename):
    """
    A context manager to temporarily redirect stdout or stderr

    e.g.:

    with stdchannel_redirected(sys.stderr, os.devnull):
        if compiler.has_function('clock_gettime', libraries=['rt']):
            libraries.append('rt')
    """

    try:
        oldstdchannel = os.dup(stdchannel.fileno())
        dest_file = open(dest_filename, 'w')
        os.dup2(dest_file.fileno(), stdchannel.fileno())

        yield
    finally:
        if oldstdchannel is not None:
            os.dup2(oldstdchannel, stdchannel.fileno())
        if dest_file is not None:
            dest_file.close()

我创建这个的背景在于这篇博客文章中。我认为这和你的差不多。
我使用了从你那里借来的代码来进行重定向(例如:os.dup2等),但我将其封装成一个上下文管理器,使其更加通用和可重复使用。
我在setup.py中像这样使用它:
with stdchannel_redirected(sys.stderr, os.devnull):
    if compiler.has_function('clock_gettime', libraries=['rt']):
        libraries.append('rt')

2

@Adam,我想指出Windows上有与/dev/null相当的东西。它是“NUL”,但最好的做法是从os.devnull获取。


1

我对编程和Python还很陌生,如果这是一个愚蠢的建议,请忽略它,但是你不能将错误消息重定向到文本文件而不是屏幕/交互式窗口/任何其他地方吗?

我相当确定我在某个地方读到过可以做类似以下的事情:

error = open('yourerrorlog.txt','w')
sys.stderr = error

再次抱歉,我可能在重复你已经知道的内容,但如果问题是当它被另一个函数(自动化)调用时,你想要错误,而手动运行时不想要错误,那么你不能只添加一个关键字参数,例如compile(arg1, arg2, manual=True),然后在你的"except:"下面添加

if manual == False: print errors to console/interactive window
else: print to error  

然后当程序调用它而不是手动调用时,只需使用compile(arg1,arg2,manual = False)来调用它,以便将其重定向到文件。


这也是我的第一个想法,但问题在于sys.stderr是Python程序的错误流,而不是编译器的。编译器是一个独立的进程,不会继承这个更改。 - Adam
如果您有想法告诉distutils以这样的方式生成编译器,使得新的错误流被保存,那么我全听着呢。 - Adam
1
实际上,我找到了一种方法来做到这一点。感谢你的推动让我去寻找更多信息,给你一个赞。 - Adam

0

运行静默模式有帮助吗?setup.py -q build

这不是对你问题的直接回答,但与你的用例相关:distutils中有一个config命令,旨在被子类化并用于检查C功能。它尚未记录,您必须阅读源代码。


我还没有尝试过安静模式,但这是由用户运行的命令,而不是由我运行的。因此,我不想要求用户使用安静模式,因为他们根本不会使用。很高兴听到他们正在研究查询C功能的东西,但我不会放弃我的脏但有效的解决方案,去追求一些未经记录的移动目标,只在某些地方起作用。 - Adam
config是一个现有的distutils命令,不是一个移动的目标。只是缺少文档。 - merwok

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接