The generic substitution builder is an improvement upon the SubstInFile2 builder. It provides a more generic way of performing substitutions and can be extended with new methods. This can be used to take *.in files and produce results from them, such as config.h from a config.h.in, an so on. It provides two common substitution methods, SubstFile and SubstHeader. These simply call the SubstGeneric builder with the desired values.

This builder has been updated to allow the SubstHeader substitutions to be quoted if desired.

The code

#!python # File: subst.py# Author: Brian A. Vanderburg II# Purpose: A generic SCons file substitution mechanism# Copyright: This file is placed in the public domain.############################################################################### Requirements##############################################################################importrefromSCons.Scriptimport*importSCons.Errors# Helper/core functions############################################################################### Do the substitutiondef_subst_file(target,source,env,pattern,replace):# Read filef=open(source,"rU")try:contents=f.read()finally:f.close()# Substitute, make sure result is a stringdefsubfn(mo):value=replace(env,mo)ifnotSCons.Util.is_String(value):raiseSCons.Errors.UserError("Substitution must be a string.")returnvaluecontents=re.sub(pattern,subfn,contents)# Write filef=open(target,"wt")try:f.write(contents)finally:f.close()# Determine which keys are useddef_subst_keys(source,pattern):# Read filef=open(source,"rU")try:contents=f.read()finally:f.close()# Determine keyskeys=[]defsubfn(mo):key=mo.group("key")ifkey:keys.append(key)return""re.sub(pattern,subfn,contents)returnkeys# Get the value of a key as a string, or None if it is not in the environmentdef_subst_value(env,key):# Why does "if key in env" result in "KeyError: 0:"?try:env[key]exceptKeyError:returnNone# Do a raw substitution so it will not replace tabs/whitespaces with a# single space. This will return a string even if the result really# isn't, such as env['HAVE_STRDUP'] = 0returnenv.subst("${%s}"%key,1)# Builder related functions############################################################################### Builder actiondef_subst_action(target,source,env):# Substitute in the filespattern=env["SUBST_PATTERN"]replace=env["SUBST_REPLACE"]for(t,s)inzip(target,source):_subst_file(str(t),str(s),env,pattern,replace)return0# Builder messagedef_subst_message(target,source,env):items=["Substituting vars from %s to %s"%(str(s),str(t))for(t,s)inzip(target,source)]return"\n".join(items)# Builder dependency emitterdef_subst_emitter(target,source,env):pattern=env["SUBST_PATTERN"]for(t,s)inzip(target,source):# When building, if a variant directory is used and source files# are being duplicated, the source file will not be duplicated yet# when this is called, so the real source must be used instead of# the duplicated sourcepath=s.srcnode().abspath# Get keys usedkeys=_subst_keys(path,pattern)d=dict()forkeyinkeys:value=_subst_value(env,key)ifnotvalueisNone:d[key]=value# Only the current target depends on this dictionaryDepends(t,SCons.Node.Python.Value(d))returntarget,source# Replace @key@ with the value of that key, and @@ with a single @##############################################################################_SubstFile_pattern="@(?P<key>\w*?)@"def_SubstFile_replace(env,mo):key=mo.group("key")ifnotkey:return"@"value=_subst_value(env,key)ifvalueisNone:raiseSCons.Errors.UserError("Error: key %s does not exist"%key)returnvaluedefSubstFile(env,target,source):returnenv.SubstGeneric(target,source,SUBST_PATTERN=_SubstFile_pattern,SUBST_REPLACE=_SubstFile_replace)# A substitutor similar to config.h header substitution# Supported patterns are:## Pattern: #define @key@# Found: #define key value# Missing: /* #define key */## Pattern: #define @key@ default# Found: #define key value# Missing: #define key default## Pattern: #undef @key@# Found: #define key value# Missing: #undef key## The "@" is used so that these defines can be used in addition to# other defines that you do not desire to be replaced. Also, each# key can specify a format to apply some formatting to the returned# value if used:## str: The returned value will be enclosed in double quotes and escaped# chr: The returned value will be enclosed in single quotes and escaped## Example:## #define @key:str@ "Default"################################################################################ Escape function_SubstHeader_escape_map={"\n":"\\n","\r":"\\r","\t":"\\t","\\":"\\\\","\0":"\\0","\"":"\\\"","\'":"\\\'"}def_SubstHeader_escape(value):# TODO: support replacement on all characters that need itresult=[]foriinvalue:ifiin_SubstHeader_escape_map:result.append(_SubstHeader_escape_map[i])else:result.append(i)return"".join(result)# Format functionsdef_SubstHeader_format_chr(value):escaped=_SubstHeader_escape(value)return"\'%s\'"%escaped[0]def_SubstHeader_format_str(value):escaped=_SubstHeader_escape(value)return"\"%s\""%escaped_SubstHeader_formats={"chr":_SubstHeader_format_chr,"str":_SubstHeader_format_str}# Actual substitution_SubstHeader_pattern="(?m)^(?P<space>\\s*?)(?P<type>#define|#undef)\\s+?@(?P<key>\w+?)(:(?P<fmt>\w+?))?@(?P<ending>.*?)$"def_SubstHeader_replace(env,mo):space=mo.group("space")type=mo.group("type")key=mo.group("key")ending=mo.group("ending")fmt=mo.group("fmt")value=_subst_value(env,key)ifnotvalueisNone:iffmtin_SubstHeader_formats:value=_SubstHeader_formats[fmt](value)# If found it is always #define key valuereturn"%s#define %s%s"%(space,key,value)# Not foundiftype=="#define":defval=ending.strip()ifdefval:# There is a default valuereturn"%s#define %s%s"%(space,key,defval)else:# There is no default valuereturn"%s/* #define %s */"%(space,key)# It was #undefreturn"%s#undef %s"%(space,key)defSubstHeader(env,target,source):returnenv.SubstGeneric(target,source,SUBST_PATTERN=_SubstHeader_pattern,SUBST_REPLACE=_SubstHeader_replace)# Create builders##############################################################################defTOOL_SUBST(env):# The generic buildersubst=SCons.Action.Action(_subst_action,_subst_message)env["BUILDERS"]["SubstGeneric"]=Builder(action=subst,emitter=_subst_emitter)# Additional onesenv.AddMethod(SubstFile,"SubstFile")env.AddMethod(SubstHeader,"SubstHeader")

SubstFile

The SubstFile builder works just like SubstInFile2, replacing any pattern of "Tom Redman@" with the value of that environment variable, and any "@@" with a single "@". If the environment variable does not exist it will result in an error instead of silently replacing it with an empty string.

SubstHeader

This works a little bit different than autotools, but the basic idea is the same. Certain matches as described in the source file will be replaced with the value from the environment if available. If not available, then it will be replaced according to the type of define statement. In addition, the names in the define statement are still surrounded with "@" to control which statements may be substituted or not. A define statement without "@" around the name will not be substituted.

// If you do not use @NAME@ then it will not be a candidate for substitution. This
// allows regular defines to be placed in a config.h.in type file as well without
// needing to make sure they are not environment variables that would be substituted.
#define COPYRIGHT "Copyright (C) 2009 Foobar"
// This provides a default value if the key is not in the environment.
#define @HAVE_STRDUP@ 0
#define @HAVE_STRCAT@ 0
// If there is no defualt value and it is not in the environment, it will be commented out
#define @WITH_WXWIDGETS@
#define @WITH_OPENGL@
// If #undef is used and the value is not in the environment, it will emit an #undef statement
#undef @HAVE_MATH_H@
#undef @HAVE_STRING_H@

config.h.in:

// If you do not use @NAME@ then it will not be a candidate for substitution. This
// allows regular defines to be placed in a config.h.in type file as well without
// needing to make sure they are not environment variables that would be substituted.
#define COPYRIGHT "Copyright (C) 2009 Foobar"
// This provides a default value if the key is not in the environment.
#define HAVE_STRDUP 1
#define HAVE_STRCAT 0
// If there is no defualt value and it is not in the environment, it will be commented out
#define WITH_WXWIDGETS
/* #define WITH_OPENGL */
// If #undef is used and the value is not in the environment, it will emit an #undef statement
#define HAVE_MATH_H 1
#undef HAVE_STRING_H

Custom Substitution

To create a custom substitution, all that is needed is a pattern to match for and a function that will be called to return the substituted value. The pattern must have a named parameter called "key" which will be used for dependency tracking. The function will take the environment and the regular expression match object as parameters. If an there is an error such as a missing variable if it is required, the function should raise a SCons.Errors.UserError

#!python pattern="#(P<key>\w*?)#"defreplace(env,mo):key=mo.group("key")ifnotkey:return"#"# "if key in env: ..." causes a KeyError for some reason instead of "key in env" being False# So test like this insteadtry:env[key]exceptKeyError:raiseSCons.Errors.UserError("Key not found: %s"%key)returnenv.subst("${%s}"%key);env.SubstGeneric("output","input.h",SUBST_PATTERN=pattern,SUBST_REPLACE=replace)