Wiki

Clone wiki

SCons / BuildNumberProcessing

The accepted way to handle an 'id.c' or 'version.c' file which includes a build number or build date/time in the executable but doesn't rebuild anything when that date/time or number changes is to use Requires(). See the SCons user guide for a fully worked example: http://www.scons.org/doc/HTML/scons-user/ch06s08.html (if in the future that link breaks, just go to the user guide and look for "Order-Only Dependencies: the Requires Function").

The rest of this page is for historical interest only.


This came from an email from Oleg.Krivosheev on the mailing list:

We have small file with static string inside which identifies build id.This build id is automatically generated during link stage.

Here is id.c

 volatile char ReleaseId[] = "Build 543, made by USER, on Oct/20/2003, 11:45:23"

The old makefile was this:

 a.exe: a.o b.o c.o
       updateReleaseId -o id.c $(USER) $(BUILDDIR) /shared/id.dat
       cc -o id.o id.c
       ld -o a.exe a.o b.o c.o id.o -lA -lB -lC

Note: updateReleaseId generates id.c with a new build number.

This solution came from AnthonyRoach:

#!python 
  env=Environment()
  env.Program('a', ['a.c', 'b.c', 'c.c', env.Object('id', 'id.c')])
  env.Command('id.c', '/release/id.dat',
              'updateReleaseId -o id.c $USER $BUILDDIR $SOURCE')

assuming USER and BUILDDIR are environment variables.


It took me a few minutes to realize this, but you can use an empty list as the source here.

So if id.c is generated entirely from the SConscript, you can say:

#!python 
  env.Command('id.c', [],
              'updateReleaseId -o id.c $USER $BUILDDIR $SOURCE')

--Stephen.Ng


Or if you want to completely emulate the way the makefile did it:

#!python 
   prog = env.Program('a', ['a.c', 'b.c', 'c.c'], CCFLAGS='id.o')
   env.AddPreAction(prog,
         ['updateReleaseId -o id.c $USER $BUILDDIR /shared/id.dat',
          'cc -c -o id.o id.c'])

The "trick" in the above is to use CCFLAGS to add the id.o target to avoid the dependency.


An alternate approach is presented here by Roberto JP:

The basic idea is to recursively call scons to build the build_id from the SConstruct file, and have the SConstruct file identify when it is called to build the build_id and then not recursively call itself.

An example is given below.

Note that this also creates the build_id using python, so it might work on windows more easily.

#!python 
import os
import sys
import SCons.Script

#so that we see the 'reading' output from scons
print "\n"


all_args=sys.argv[1:]
parser=SCons.Script.OptParser()
options,targets = parser.parse_args(all_args)


# this function returns a string containing the
# text of the build_id.c file.
#
# essentially, it creates a static character string with the
# build time, date, user, and machine.
#
def get_build_id_file_text():
        import socket
        import getpass
        import time

        build_time = time.strftime("%Y_%m_%d__%H_%M_%S", time.gmtime())
        build_username = getpass.getuser()
        #if windows (don't know how to ask that yet.. then
        #username = win32api.GetUserName()
        build_hostname = socket.gethostname()
        build_id_statements = '//this file is automatically generated \n'
        build_id_statements+= 'static char* build_id="'
        build_id_statements+= 'build_id' + '|' + build_time + '|' + build_username + '@' + build_hostname
        build_id_statements+= '";\n'
        return build_id_statements

# we use a separate build_dir
# but for here, we'll call that '.'
build_dir="."


Clean('','build_id.c')
Clean('','build_id.o')

if('build_id' in targets) :

        try : os.mkdir(build_dir)
        except :  print("build_dir already exists")

        build_id_file= file( build_dir + "/build_id.c", 'w' )
        build_id_file.write( get_build_id_file_text() )
        build_id_file.close( )

        # this is for a static build..
        build_id=AlwaysBuild(      Object(build_dir + '/build_id.o',build_dir + '/build_id.c'))

        # for a shared build you may want to make it a 'SharedObject', as in the commented out line below.
        #build_id=AlwaysBuild(SharedObject(build_dir + '/build_id.os',build_dir + '/build_id.c'))

        Alias('build_id',build_id)
else :
        # the following line invokes scons to build the build_id.
        os.system('scons build_id')

        env=Environment()
        env['build_dir']=build_dir

        # add the build_id.o flag to the LINKFLAGS.
        env['LINKFLAGS'] +=  [ build_dir + '/build_id.o']

        # and now we'd put our 'regular' scons statements.
        # note that you'd want to use env.COMMAND(f00)
        # as opposed to COMMAND(foo)
        # so that the linkflags are used.

        env.Program('foo','foo.c')
        # if you do 'strings foo' on the executable created, you should see the build_id text.


        #... etc ...

#so that we see the 'done reading' output from scons
print "\n"

Here's a related problem that I wasn't able to solve with the above techniques. I have a file Version.cpp that serves a similar purpose to id.c in the previous examples:

// Version.cpp

#define BUILD_DATE __TIME__ " " __DATE__
#define BUILD_STRING  "[built " BUILD_DATE "]"

const char *buildString() { return BUILD_STRING; }
const char *buildDate() { return BUILD_DATE; }

In this case, the file contents automatically change whenever it is recompiled, and I want that recompile to be forced whenever the library that contains Version.o is rebuilt. The rule for building the library looks something like this:

#!python 
SOURCES = [
  'Bar.cpp',
  'Foo.cpp',
  'Version.cpp',
]

static_lib = env.StaticLibrary('foobar', SOURCES)

Now dig into the internals a bit to expand the dependecies of Version.o to match those of the shared library:

#!python 
from SCons.Node import NodeList
def MakeVersionDeps(env, target, prefix='Version.'):
  # TODO add error handling/reporting
  # isinstance(target,NodeList) is needed for scons > 0.96
  if type(target) == type([]) or isinstance(target,NodeList):
    target = target[0]
  version = [child for child in target.children() if str(child).startswith(prefix)]
  others = [child for child in target.children() if not str(child).startswith(prefix)]
  if version:
    env.Depends(version, others)

MakeVersionDeps(env, static_lib)

MakeVersionDeps() also works with shared libraries, and probably other target types.


Here's another solution:

#!python 
def get_build_id():
     return "my_unique_build_id_string"

def generate_build_id(env, target, source):
     out = open(target[0].path, "w")
     out.write(source[0].get_contents())
     out.close()

Command("build_id.c", [Value(get_build_id())], generate_build_id)

This causes build_id.c to be regenerated only if the build_id changes, without the need for any external programs or similar to update the build_id.c file. The drawback is that it if the build id contains a string which is difference for each scons run, then the target will always be rebuilt with a new build id.


This page is also relevant for including time stamps and date stamps in builds (so the built code contains the build date or time). I mention this here so people can find this page if that's what they're looking for.

Updated