Programs and shared libraries can contain symbol information that increase the size of the data to redistribute and that facilitates reverse-engineering. The linkers/compilers do not provide a way of stripping symbols while creating the files, so in practice the strip program needs to be executed on the resulting binary files. The build process must then place barriers to prevent usage of such binaries before they are ready.

A fairly common approach consists in running strip only on files that are installed. Overriding the method copy_fun on the installation class provides a coarse-grained way of achieving this. In the following example, the strip command is called on files comes from a link task:

import shutil, os
from waflib import Build, Utils, Context

def copy_fun(self, src, tgt):
    shutil.copy2(src, tgt)
    os.chmod(tgt, self.chmod)

    if getattr(self.generator, 'link_task', None):
        if self.generator.link_task.outputs[0] in self.inputs:
            self.generator.bld.cmd_and_log('strip %s' % tgt, quiet=Context.BOTH)
Build.inst.copy_fun = copy_fun

If stripping is required during the build phase, then the build order must be set so that dependent tasks wait for the binaries to be ready. A reliable implementation may be difficult to achieve for partial builds if the strip operation is modeled as a Task object as the task does not know the full list of downstream dependencies beforehand. The following hack hack provides a rather easy way to force a particular order though. In the following example, both inputs and outputs for the strip task are set to the same file:

from waflib import TaskGen

@TaskGen.feature('cshlib', 'cxxshlib', 'cprogram', 'cxxprogram')
@TaskGen.after('apply_link')
def add_strip_task(self):
    if getattr(self, 'link_task', None):
        exe_node = self.link_task.outputs[0]
        # special case: same inputs and outputs for a task!
        strip_task = self.create_task('strip', exe_node, exe_node)

The main drawback of this solution is that a deadlock will be observed if several post-processing operations are declared for the same file. Besides that, the strip task object is not really necessary in the first place; removing it can significantly reduce the amount of console logs for a whole build.

My favorite approach consists in chaining the run method through inheritance. In the example below, the function wrap_compiled_task creates subclasses with the same name as the original. A Python metaclass bound to parent Task class translates the run_str attribute into a run method so that a long python function does not need to be written down. That metaclass also registers the last class created so that cls3 replaces cls1 in Task.classes[classname] as default. One must be careful to load C/C++/Fortran Waf tools first else the code will not find any class to subclass:

from waflib import Task

def wrap_compiled_task(classname):
    # create subclasses and override the method 'run'
    #
    cls1 = Task.classes[classname]
    cls2 = type(classname, (cls1,), {'run_str': '${STRIP} ${TGT[0].abspath()}'})
    cls3 = type(classname, (cls2,), {})

    def run_all(self):
        if self.env.NO_STRIPPING:
            return cls1.run(self)
        ret = cls1.run(self)
        if ret:
            return ret
        return cls2.run(self)
    cls3.run = run_all

for k in 'cprogram cshlib cxxprogram cxxshlib fcprogram fcshlib'.split():
    if k in Task.classes:
        wrap_compiled_task(k)

The techniques described above above are not exclusively for stripping binaries though. They may be precious in situations where built files need to be modified after a compiler was executed. The complete examples for this post can be found in the following folder.