%scons; %builders-mod; %functions-mod; %tools-mod; %variables-mod; ]> Extending &SCons;: Writing Your Own Scanners &SCons; has built-in &Scanners; that know how to look in C/C++, Fortran, D, IDL, LaTeX, Python and SWIG source files for information about other files that targets built from those files depend on. For example, if you have a file format which uses #include to specify files which should be included into the source file when it is processed, you can use an existing scanner already included in &SCons;. You can use the same mechanisms that &SCons; uses to create its built-in Scanners to write Scanners of your own for file types that &SCons; does not know how to scan "out of the box."
A Simple Scanner Example Suppose, for example, that we want to create a simple &Scanner; for .k files. A .k file contains some text that will be processed, and can include other files on lines that begin with include followed by a file name: include filename.k Scanning a file will be handled by a Python function that you must supply. Here is a function that will use the Python re module to scan for the include lines in our example: import re include_re = re.compile(r'^include\s+(\S+)$', re.M) def kfile_scan(node, env, path, arg=None): contents = node.get_text_contents() return env.File(include_re.findall(contents)) It is important to note that you have to return a list of File nodes from the scanner function, simple strings for the file names won't do. As in the examples we are showing here, you can use the &f-link-File; function of your current &consenv; in order to create nodes on the fly from a sequence of file names with relative paths. The scanner function must accept the four specified arguments and return a list of implicit dependencies. Presumably, these would be dependencies found from examining the contents of the file, although the function can perform any manipulation at all to generate the list of dependencies. node An &SCons; node object representing the file being scanned. The path name to the file can be used by converting the node to a string using the str function, or an internal &SCons; get_text_contents object method can be used to fetch the contents. env The &consenv; in effect for this scan. The scanner function may choose to use &consvars; from this environment to affect its behavior. path A list of directories that form the search path for included files for this Scanner. This is how &SCons; handles the &cv-link-CPPPATH; and &cv-link-LIBPATH; variables. arg An optional argument that can be passed to this scanner function when it is called from a scanner instance. The argument is only supplied if it was given when the scanner instance is created (see the manpage section "Scanner Objects"). This can be useful, for example, to distinguish which scanner type called us, if the function might be bound to several scanner objects. Since the argument is only supplied in the function call if it was defined for that scanner, the function needs to be prepared to possibly be called in different ways if multiple scanners are expected to use this function - giving the parameter a default value as shown above is a good way to do this. If the function to scanner relationship will be 1:1, just make sure they match. A scanner object is created using the &f-link-Scanner; function, which typically takes an skeys argument to associate a file suffix with this Scanner. The scanner object must then be associated with the &cv-link-SCANNERS; &consvar; in the current &consenv;, typically by using the &f-link-Append; method: kscan = Scanner(function=kfile_scan, skeys=['.k']) env.Append(SCANNERS=kscan) Let's put this all together. Our new file type, with the .k suffix, will be processed by a command named kprocess, which lives in non-standard location /usr/local/bin, so we add that path to the execution environment so &SCons; can find it. Here's what it looks like: import re include_re = re.compile(r'^include\s+(\S+)$', re.M) def kfile_scan(node, env, path): contents = node.get_text_contents() includes = include_re.findall(contents) return env.File(includes) kscan = Scanner(function=kfile_scan, skeys=['.k']) env = Environment() env.AppendENVPath('PATH', '__ROOT__/usr/local/bin') env.Append(SCANNERS=kscan) env.Command('foo', 'foo.k', 'kprocess < $SOURCES > $TARGET') some initial text include other_file some other text cat Assume a foo.k file like this: Now if we run &scons; we can see that the scanner works - it identified the dependency other_file via the detected include line, although we get an error message because we forgot to create that file! scons -Q
Adding a search path to a Scanner: &FindPathDirs; If the build tool in question will use a path variable to search for included files or other dependencies, then the &Scanner; will need to take that path variable into account as well - the same way &cv-link-CPPPATH; is used for files processed by the C Preprocessor (used for C, C++, Fortran and others). Path variables may be lists of nodes or semicolon-separated strings (&SCons; uses a semicolon here irrespective of the pathlist separator used by the native operating system), and may contain &consvars; to be expanded. A Scanner can take a path_function to process such a path variable; the function produces a tuple of paths that is passed to the scanner function as its path parameter. To make this easy, &SCons; provides the premade &f-link-FindPathDirs; function which returns a callable to expand a given path variable (given as an &SCons; &consvar; name) to a tuple of paths at the time the Scanner is called. Deferring evaluation until that point allows, for instance, the path to contain &cv-link-TARGET; references which differ for each file scanned. Using &FindPathDirs; is easy. Continuing the above example, using $KPATH as the &consvar; to hold the paths (analogous to &cv-link-CPPPATH;), we just modify the call to the &f-link-Scanner; factory function to include a path_function keyword argument: kscan = Scanner( function=kfile_scan, skeys=['.k'], path_function=FindPathDirs('KPATH'), ) &FindPathDirs; is called when the Scanner is created, and the callable object it returns is stored as an attribute in the scanner. When the scanner is invoked, it calls that object, which processes the $KPATH from the current &consenv;, doing necessary expansions and, if necessary, adds related repository and variant directories, producing a (possibly empty) tuple of paths that is passed on to the scanner function. The scanner function is then responsible for using that list of paths to locate the include files identified by the scan. The next section will show an example of that. As a side note, the returned method stores the path in an efficient way so lookups are fast even when variable substitutions may be needed. This is important since many files get scanned in a typical build.
Using scanners with Builders One approach for introducing a &Scanner; into the build is in conjunction with a &Builder;. There are two relevant optional parameters we can use when creating a Builder: source_scanner and target_scanner. source_scanner is used for scanning source files, and target_scanner is used for scanning the target once it is generated. import os, re include_re = re.compile(r"^include\s+(\S+)$", re.M) def kfile_scan(node, env, path, arg=None): includes = include_re.findall(node.get_text_contents()) print(f"DEBUG: scan of {str(node)!r} found {includes}") deps = [] for inc in includes: for dir in path: file = str(dir) + os.sep + inc if os.path.exists(file): deps.append(file) break print(f"DEBUG: scanned dependencies found: {deps}") return env.File(deps) kscan = Scanner( function=kfile_scan, skeys=[".k"], path_function=FindPathDirs("KPATH"), ) def build_function(target, source, env): # Code to build "target" from "source" return None bld = Builder( action=build_function, suffix=".k", source_scanner=kscan, src_suffix=".input", ) env = Environment(BUILDERS={"KFile": bld}, KPATH="inc") env.KFile("file") some initial text include other_file some other text text to include Running this example would only show that the stub build_function is getting called, so some debug prints were added to the scanner function, just to show the scanner is being invoked. scons -Q The path-search implementation in kfile_scan works, but is quite simple-minded - a production scanner will probably do something more sophisticated. An emitter function can modify the list of sources or targets passed to the action function when the Builder is triggered. A scanner function will not affect the list of sources or targets seen by the Builder during the build action. The scanner function will, however, affect if the Builder should rebuild (if any of the files sourced by the Scanner have changed for example).