到目录为止,我们已经看到了SCons是如何一次性编译的。但是SCons这样的编译工具的一个主要的功能就是当源文件改变的时候,只需要重新编译那些修改的文件,而不会浪费时间去重新编译那些不需要重新编译的东西。如下所示:
env = Environment()
def config_file_decider(dependency, target, prev_ni):
import os.path
# We always have to init the .csig value...
dep_csig = dependency.get_csig()
# .csig may not exist, because no target was built yet...
if 'csig' not in dir(prev_ni):
return True
# Target file may not exist yet
if not os.path.exists(str(target.abspath)):
return True
if dep_csig != prev_ni.csig:
# Some change on source file => update installed one
return True
return False
def update_file():
f = open("test.txt","a")
f.write("some line\n")
f.close()
update_file()
# Activate our own decider function
env.Decider(config_file_decider)
env.Install("install","test.txt")
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
scons: '.' is up to date.
第二次执行的时候,SCons根据当前的hello.c源文件判断出hello程序是最新的,避免了重新编译。
1、决定一个输入文件何时发生了改变:Decider函数
默认情况下,SCons通过每个文件内容的MD5签名,或者校验和来判断文件是否是最新的,当然你也可以配置SCons使用文件的修改时间来判断。你甚至可以指定你自己的Python函数来决定一个输入文件是否发生了改变。
1.1、使用MD5签名来决定一个文件是否改变
默认情况下,SCons根据文件内容的MD5校验和而不是文件的修改时间来决定文件是否改变。如果你想更新文件的修改时间,来使得SCons重新编译,那么会失望的。如下所示:
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% touch hello.c
% scons -Q hello
scons: `hello' is up to date.
cc -o hello.o -c hello.c
cc -o hello hello.o
% touch hello.c
% scons -Q hello
scons: `hello' is up to date.
上面的例子中即使文件的修改时间变了,SCons认为文件的内容没有改变,所以不需要重新编译。但是如果文件的内容改变了,SCons会侦测到并且重新编译的:
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% edit hello.c
[CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
cc -o hello.o -c hello.c
cc -o hello hello.o
% edit hello.c
[CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
你也可以显示指定使用MD5签名,使用Decider函数:
Program('hello.c')
Decider('MD5')
1.1.1、使用MD5签名的衍生
使用Md5签名去决定一个输入文件是否改变,有一个好处:如果一个源文件已经改变了,但是由它重新编译出来的目标文件的内容和由它修改前编译出来的目标文件一样,那么那些依赖这个重新编译的但是内容没变的目标文件的其他目标文件是不需要重新编译的。
例如,一个用户仅仅改变了hello.c文件中的注释,那么重新编译出来的hello.o文件肯定是不变的。SCons将不会重新编译hello程序:
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% edit hello.c
[CHANGE A COMMENT IN hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
scons: `hello' is up to date.
cc -o hello.o -c hello.c
cc -o hello hello.o
% edit hello.c
[CHANGE A COMMENT IN hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
scons: `hello' is up to date.
1.2、使用时间戳(Time Stamps)来决定一个文件是否改变
SCons允许使用两种方式使用时间戳来决定一个输入文件是否已经改变。
最熟悉的方式就是Make使用时间戳的方式:如果一个源文件的修改时间比目标文件新,SCons认为这个目标文件应该重新编译。调用Decider函数如下:
Object('hello.c')
Decider('timestamp-newer')
并且因为这个行为和Make的一样,你调用Decider函数的时候可以用make替代timestamp-newer:
Object('hello.c')
Decider('make')
使用和Make一样时间戳的一个缺点就是如果一个输入文件的修改时间突然变得比一个目标文件旧,这个目标文件将不会被重新编译。例如,如果一个源文件的一个旧的拷贝从一个备份中恢复出来,恢复出来的文件的内容可能不同,但是目标文件将不会重新编译因为恢复出来的源文件的修改时间不比目标文件文件新。
因为SCons实际上存储了源文件的时间戳信息,它可以处理这种情况,通过检查源文件时间戳的精确匹配,而不是仅仅判断源文件是否比目标文件新。示例如下:
Object('hello.c')
Decider('timestamp-match')
1.3、同时使用MD5签名和时间戳来判断一个文件是否改变
SCons提供了一种方式,使用文件内容的MD5校验和,但是仅仅当文件的时间戳改变的时候去读文件的内容:
Program('hello.c')
Decider('MD5-timestamp')
使用Decider('MD5-timestamp')的唯一缺点就是SCons将不会重新编译一个目标文件,如果SCons编译这个文件后的一秒以内源文件被修改了。
1.4、编写你自己的Decider函数
我们传递给Decider函数的不同的字符串实际上是告诉SCons去选择内部已经实现的决定文件是否改变的函数。我们也可以提供自己的函数来决定一个依赖是否已经改变。
例如,假设我们有一个输入文件,其包含了很多数据,有特定的格式,这个文件被用来重新编译许多不同的目标文件,但是每个目标文件仅仅依赖这个输入文件的一个特定的区域。我们希望每个目标文件仅仅依赖自己在输入文件中的区域。但是,因为这个输入文件可能包含了很多数据,我们想仅仅在时间戳改变的时候才打开这个文件。这个可以通过自定义的Decider函数实现:
Program('hello.c')
def decide_if_changed(dependency,target,prev_ni):
if self.get_timestamp()!=prev_ni.timestamp:
dep=str(dependency)
tgt=str(target)
if specific_part_of_file_has_changed(dep,tgt):
return True
return False
Decider(decide_if_changed)
在函数定义中,depandency(输入文件)是第一个参数,然后是target。它们都是作为SCons节点对象传递给函数的,所以我们需要使用str()转换成字符串。
第三个参数,prev_ni,是一个对象,这个对象记录了目标文件上次编译时所依赖的签名和时间戳信息。prev_ni对象可以记录不同的信息,取决于dependency参数所表示的东西的类型。对于普通的文件,prev_ni对象有以下的属性:
.csig:target上次编译时依赖的dependency文件内容的内容签名或MD5校验和
.size:dependency文件的字节大小
.timestamp:dependency文件的修改时间
注意如果Decider函数中的一些参数没有影响到你决定dependency文件是否改变,你忽略掉这些参数是很正常的事情。
以上的三个属性在第一次运行的时候,可能不会出现。如果没有编译过,没有target创建过也没有.sconsign DB文件存在过。所以,最好总是检查prev_ni的属性是否可用。
以下是一个基于csig的decider函数的例子,注意在每次函数调用的时候,dependency文件的签名信息是怎么样通过get_csig初始化的:
env = Environment()
def config_file_decider(dependency, target, prev_ni):
import os.path
# We always have to init the .csig value...
dep_csig = dependency.get_csig()
# .csig may not exist, because no target was built yet...
if 'csig' not in dir(prev_ni):
return True
# Target file may not exist yet
if not os.path.exists(str(target.abspath)):
return True
if dep_csig != prev_ni.csig:
# Some change on source file => update installed one
return True
return False
def update_file():
f = open("test.txt","a")
f.write("some line\n")
f.close()
update_file()
# Activate our own decider function
env.Decider(config_file_decider)
env.Install("install","test.txt")
1.5、混合使用不同的方式来决定一个文件是否改变
有些时候,你想为不同的目标程序配置不同的选项。你可以使用env.Decider方法影响在指定construction环境下编译的目标程序。
例如,如果我们想使用MD5校验和编译一个程序,另一个使用文件的修改时间:
env1=Environment(CPPPATH=['.'])
env2=env1.Clone()
env2.Decider('timestamp-match')
env1.Program('prog-MD5','program1.c')
env2.Program('prog-timestamp','program2.c')
2、决定一个输入文件是否改变的旧函数
SCons2.0之前的两个函数SourceSignatures和TargetSignatures,现在不建议使用了。
3、隐式依赖:$CPPPATH Construction变量
现在假设"Hello,World!"程序有一个#include行需要包含hello.h头文件:
#include <hello.h>
int main()
{
printf("Hello, %s!\n",string);
}
并且,hello.h文件如下:
#define string "world"
在这种情况下,我们希望SCons能够认识到,如果hello.h文件的内容发生改变,那么hello程序必须重新编译。我们需要修改SConstruct文件如下:
Program('hello.c', CPPPATH='.')
$CPPPATH告诉SCons去当前目录('.')查看那些被C源文件(.c或.h文件)包含的文件。
% scons -Q hello
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% edit hello.h
[CHANGE THE CONTENTS OF hello.h]
% scons -Q hello
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% edit hello.h
[CHANGE THE CONTENTS OF hello.h]
% scons -Q hello
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
首先注意到,SCons根据$CPPPATH变量增加了-I.参数,使得编译器在当前目录查找hello.h文件。
其次,SCons知道hello程序需要重新编译,因为它扫描了hello.c文件的内容,知道hello.h文件被包含。SCons将这些记录为目标文件的隐式依赖,当hello.h文件改变的时候,SCons就会重新编译hello程序。
就像$LIBPATH变量,$CPPPATH也可能是一个目录列表,或者一个被系统特定路径分隔符分隔的字符串。
Program('hello.c', CPPPATH=['include', '/home/project/inc'])
4、缓存隐式依赖
扫描每个文件的#include行会消耗额外的处理时间。
SCons让你可以缓存它扫描找到的隐式依赖,在以后的编译中可直接使用。这需要在命令行中指定--implicit-cache选项:
% scons -Q --implicit-cache hello
如果你不想每次在命令行中指定--implicit-cache选项,你可以在SConscript文件中设置implicit-cache选项使其成为默认的行为:
SetOption('implicit-cache', 1)
SCons默认情况下不缓存隐式依赖,因为--implicit-cache使得SCons在最后运行的时候,只是简单的使用已经存储的隐式依赖,而不会检查那些依赖还是不是仍然正确。在如下的情况中,--implicit-cache可能使得SCons的重新编译不正确:
1>当--implicit-cache被使用,SCons将会忽略$CPPPATH或$LIBPATH中发生的一些变化。如果$CPPPATH的一个改变,使得不同目录下的内容不相同但文件名相同的文件被使用,SCons也不会重新编译。
2>当--implicit-cache被使用,如果一个同名文件被添加到一个目录,这个目录在搜索路径中的位置在同名文件上次被找到所在的目录之前,SCons将侦测不到。
4.1、--implicit-deps-changed选项
当使用缓存隐式依赖的时候,有些时候你想让SCons重新扫描它之前缓存的依赖。你可以运行--implicit-deps-changed选项:
% scons -Q --implicit-deps-changed hello
4.2、--implicit-deps-unchanged选项
默认情况下在使用缓存隐式依赖的时候,SCons会注意到当一个文件已经被修改的时候,就会重新扫描文件更新隐式依赖信息。有些时候,你可能想即使源文件改变了,但仍然让SCons使用缓存的隐式依赖。你可以使用--implicit-deps-unchanged选项:
% scons -Q --implicit-deps-unchanged hello
5、显示依赖:Depends函数
有些时候一个文件依赖另一个文件,是不会被SCons扫描器侦测到的。对于这种情况,SCons允许你显示指定一个文件依赖另一个文件,并且无论何时被依赖文件改变的时候,需要重新编译。这个需要使用Depends方法:
hello=Program("hello.c")
Depends(hello,'other_file')
注意Depends方法的第二个参数也可以是一个节点对象列表:
hello=Program('hello.c')
goodbye=Program('goodbye.c')
Depends(hello,goodbye)
在这种情况下,被依赖的对象会在目标对象之前编译:
% scons -Q hello
cc -c goodbye.c -o goodbye.o
cc -o goodbye goodbye.o
cc -c hello.c -o hello.o
cc -o hello hello.o
6、来自外部文件的依赖:ParseDepends函数
SCons针对许多语言,有内置的扫描器。有些时候,由于扫描器实现的缺陷,扫描器不能提取出某些隐式依赖。
下面的例子说明了内置的C扫描器不能提取一个头文件的隐式依赖:
#define FOO_HEADER <foo.h>
#include FOO_HEADER
int main()
{
return FOO;
}
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% edit foo.h
% scons -Q
scons: '.' is up to date.
显然,扫描器没有发现头文件的依赖。这个扫描器不是一个完备的C预处理器,没有扩展宏。
在这种情况下,你可能想使用编译器提取隐式依赖。ParseDepends可以解析编译器输出的内容,然后显示建立所有的依赖。
下面的例子使用ParseDepends处理一个编译器产生的依赖文件,这个依赖文件是在编译目标文件的时候作为副作用产生的:
obj=Object('hello.c', CCFLAGS='-MD -MF hello.d', CPPPATH='.')
SideEffect('hello.d',obj)
ParseDepends('hello.d')
Program('hello', obj)
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
cc -o hello hello.o
% edit foo.h
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
从一个编译器产生的.d文件解析依赖有一个先有鸡还是先有蛋的问题,会引发不必要的重新编译:
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
cc -o hello hello.o
% scons -Q --debug=explain
scons: rebuilding `hello.o' because `foo.h' is a new dependency
cc -o hello.o -c -MD -MF hello.d -I. hello.c
% scons -Q
scons: `.' is up to date.
cc -o hello.o -c -MD -MF hello.d -I. hello.c
cc -o hello hello.o
% scons -Q --debug=explain
scons: rebuilding `hello.o' because `foo.h' is a new dependency
cc -o hello.o -c -MD -MF hello.d -I. hello.c
% scons -Q
scons: `.' is up to date.
第一次运行的时候,在编译目标文件的时候,依赖文件产生了。在那个时候,SCons不知道foo.h的依赖。第二次运行的时候,目标文件被重新生成因为foo.h被发现是一个新的依赖。
ParseDepends在调用的时候立即读取指定的文件,如果文件不存在马上返回。在编译过程中产生的依赖文件不会被再次自动解析。因此,在同样的编译过程中,编译器提取的依赖不会被存储到签名数据库中。这个ParseDepends的缺陷导致不必要的重新编译。因此,仅仅在扫描器对于某种语言不可用或针对特定的任务不够强大的情况下,才使用ParseDepends。
7、忽略依赖:Ignore函数
有些时候,即使一个依赖的文件改变了,也不想要重新编译。在这种情况下,你需要告诉SCons忽略依赖,如下所示:
hello_obj=Object('hello.c')
hello = Program(hello_obj)
Ignore(hello_obj, 'hello.h')
% scons -Q hello
cc -c -o hello.o hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% edit hello.h
[CHANGE THE CONTENTS OF hello.h]
% scons -Q hello
scons: `hello' is up to date.
hello = Program(hello_obj)
Ignore(hello_obj, 'hello.h')
% scons -Q hello
cc -c -o hello.o hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% edit hello.h
[CHANGE THE CONTENTS OF hello.h]
% scons -Q hello
scons: `hello' is up to date.
上面的例子是人为做作的,因为在真实情况下,如果hello.h文件改变了,你不可能不想重新编译hello程序。一个更真实的例子可能是,如果hello程序在一个目录下被编译,这个目录在多个系统中共享,多个系统有不同的stdio.h的拷贝。在这种情况下,SCons将会注意到不同系统的stdio.h拷贝的不同,当你每次改变系统的时候,重新编译hello。你可以避免这些重新编译,如下所示:
hello=Program('hello.c', CPPPATH=['/usr/include'])
Ignore(hello, '/usr/include/stdio.h')
Ignore也可以用来阻止在默认编译情况下文件的产生。这是因为目录依赖它们的内容。所以为了忽略默认编译时产生的文件,你指定这个目录忽略产生的文件。注意到如果用户在scons命令行中请求目标程序,或者这个文件是默认编译情况下另一个文件的依赖,那么这个文件仍然会被编译。
hello_obj=Object('hello.c')
hello = Program(hello_obj)
Ignore('.',[hello,hello_obj])
% scons -Q
scons: `.' is up to date.
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
hello = Program(hello_obj)
Ignore('.',[hello,hello_obj])
% scons -Q
scons: `.' is up to date.
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
8、顺序依赖:Requires函数
有时候,需要指定某一个文件或目录必须在某些目标程序被编译之前被编译或创建,但是那个文件或目录如果发生了改变,那个目标程序不需要重新编译。这样一种关系叫做顺序依赖(order-only dependency)因为它仅仅影响事物编译的顺序,它不是一种严格意义上的依赖关系,因为目标程序不需要随着依赖文件的改变而改变。
例如,你想在每次编译的时候创建一个文件用来标识编译执行的时间,版本号等等信息。这个版本文件的内容在每次编译的时候都会改变。如果你指定一个正常的依赖关系,那么每个依赖这个文件的程序在你每次运行SCons的时候都会重新编译。例如,我们可以使用一些Python代码在SConstruct文件中创建一个新的version.c文件,version.c文件会记录我们每次运行SCons的当前日期,然后链接到一个程序:
import time
version_c_text="""
char *date="%s"
""" % time.ctime(time.time())
open('version.c', 'w').write(version_c_text)
hello=Program(['hello.c', 'version.c'])
如果我们将version.c作为一个实际的源文件,那么version.o文件在我们每次运行SCons的时候都会重新编译,并且hello可执行程序每次会重新链接。
% scons -Q hello
cc -o hello.o -c hello.c
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
cc -o hello.o -c hello.c
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
我们的解决方案是使用Requires函数指定version.o在链接之前必须重新编译,但是version.o的改变不需要引发hello可执行程序重新链接:
import time
version_c_text = """
char *date = "%s";
""" % time.ctime(time.time())
open('version.c', 'w').write(version_c_text)
version_obj = Object('version.c')
hello = Program('hello.c',
LINKFLAGS = str(version_obj[0]))
Requires(hello, version_obj)
version_c_text = """
char *date = "%s";
""" % time.ctime(time.time())
open('version.c', 'w').write(version_c_text)
version_obj = Object('version.c')
hello = Program('hello.c',
LINKFLAGS = str(version_obj[0]))
Requires(hello, version_obj)
注意到因为我们不再将version.c作为hello程序的源文件,我们必须找到其他的方式使其可以链接。在这个例子中,我们将对象文件名放到$LINKFLAGS变量中,因为$LINKFLAGS已经包含在$LINKCOM命令行中了。
通过这些改变,当hello.c改变的时候,hello可执行程序才重新链接:
% scons -Q hello
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: `hello' is up to date.
% sleep 1
% edit hello.c
[CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: `hello' is up to date.
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: `hello' is up to date.
% sleep 1
% edit hello.c
[CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: `hello' is up to date.
9、AlwaysBuild函数
当一个文件传递给AlwaysBuild方法时,
hello=Program('hello.c')
AlwaysBuild(hello)
那么指定的目标文件将总是被认为是过时的,并且被重新编译:
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
cc -o hello hello.o
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
cc -o hello hello.o
AlwaysBuild函数并不意味着每次SCons被调用的时候,目标文件会被重新编译。在命令行中指定某个其他的目标,这个目标自身不依赖AlwaysBuild的目标程序,这个目标程序仅仅当它的依赖改变的时候才会重新编译:
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello.o
scons: `hello.o' is up to date.
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello.o
scons: `hello.o' is up to date.