MSVC compiler has a nice feature - if you build DLL, all symbols should be resolved. After googling for a few hours, I didn't find the necessary combination of GCC flags, to achieve same result for shared libraries. So I created a custom builder based on ldd and c++filt utilities, which dumps unresolved symbols to a text file.
It is not the perfect solution, but it is good enough for me and may be it will be useful for you too.
As a side note: I had more luck with looking for such feature. Linker (at least on linux) has --no-undefined option which does exactly this. When you want to pass it to gcc or g++ you have to use -Wl,--no-undefined to indicate that option has to be passed to linker. - Tomasz Gajewski
The builder code:
1 import os
2 import re
3 import subprocess
4 import SCons.Action
5 import SCons.Builder
6 import SCons.Scanner
7
8 class dependencies_dumper_t:
9 unresolved_lib_re = re.compile( r'^\s+(?P<lib>.*)\s\=\>\snot found.*' )
10 undefined_symbol_re = re.compile( r'^\s*undefined symbol\:\s+(?P<symbol>.*)\s\(.*\)' )
11
12 def __init__( self, target, source, env ):
13 self.env = env
14 self.target = target
15 self.source = source
16
17 self.new_os_env = os.environ.copy()
18 self.new_os_env['LD_LIBRARY_PATH'] = env['ENV']['LD_LIBRARY_PATH']
19 #self.new_os_env['PATH'] = env.get( 'PATH', os.environ.get( 'PATH', '' ) )
20 #todo?: may be I need to add source file directory to LD_LIBRARY_PATH
21
22 def write2log( self, msg ):
23 print msg
24
25 def __run_ldd( self ):
26 try:
27 tf = file( self.target[0].abspath, 'w+r' )
28 args = [ 'ldd', '-r', self.source[0].abspath ]
29 ldd_prs = subprocess.Popen( ' '.join( args )
30 , shell=True
31 , close_fds=True
32 , bufsize=1024*50
33 , stdout=tf
34 , stderr=tf
35 , cwd=os.path.dirname( self.source[0].abspath )
36 , env=self.new_os_env)
37
38 ldd_prs.wait()
39
40 if 0 != ldd_prs.returncode:
41 raise RuntimeError( 'Error during execution of ldd process. Error code: %d'
42 % ldd_prs.return_code )
43 tf.seek(0)
44 output = tf.read()
45 tf.close()
46 lines = output.split( '\n' )
47 lines = map( lambda s: s.strip(), lines )
48 return filter( None, lines )
49 except Exception, err:
50 print 'error executing ldd process: ', str(err)
51 raise
52
53 def __demangle_symbol( self, symbol ):
54 try:
55 tmp_f_name = os.path.join( self.env['AIS_B_CFG' ]['TEMP_PATH']
56 , os.path.basename( self.source[0].abspath ) + '.cppfilt.txt' )
57 tmp_f = file( self.env.File( tmp_f_name ).abspath, 'w+r' )
58 args = [ 'c++filt', symbol ]
59 prs = subprocess.Popen( ' '.join( args )
60 , shell=True
61 , close_fds=True
62 , stdout=tmp_f
63 , stderr=tmp_f )
64
65 prs.wait()
66
67 if 0 != prs.returncode:
68 raise RuntimeError( 'Error during execution of c++filt process. Error code: %d'
69 % prs.return_code )
70 tmp_f.seek(0)
71 output = tmp_f.read().strip()
72 tmp_f.close()
73 return '%s { %s }' % ( output, symbol )
74 except Exception, err:
75 print 'error executing c++filt process: ', str(err)
76 return symbol
77
78 def __list_unknown( self, ldd_result, pattern, gname ):
79 unknown = []
80 for l in ldd_result:
81 m = pattern.match( l )
82 if not m:
83 continue
84 unknown.append( m.group( gname ) )
85 return unknown
86
87 def dump(self):
88 ldd_lines = self.__run_ldd()
89 libs = self.__list_unknown( ldd_lines, self.unresolved_lib_re, 'lib' )
90 symbols = self.__list_unknown( ldd_lines, self.undefined_symbol_re, 'symbol' )
91 tf = file( self.target[0].abspath, 'w+' )
92 if libs or symbols:
93 tf.write( 'LD_LIBRARY_PATH:\n' )
94 for path in self.new_os_env['LD_LIBRARY_PATH'].split(':'):
95 tf.write( '\t' + path + '\n' )
96 if libs:
97 tf.write( 'unresolved libs: \n' )
98 for lib in libs:
99 tf.write( '\t' + lib + '\n' )
100 if symbols:
101 tf.write( 'undefined symbols: \n' )
102 for symbol in symbols:
103 tf.write( '\t' + self.__demangle_symbol( symbol ) + '\n' )
104 tf.close()
105
106 def build_it( target, source, env ):
107 """
108 source - executable or shared object
109 target - text file, which contains list of other shared libraries it
110 depends on and list of undefined symbols. If there are no
111 undefined symbols, the file will be empty
112
113 LD_LIBRARY_PATH environment variable will be used to find out the location
114 of other shared libraries
115
116 PATH environment variable will be used to find out the location of 'ldd'
117 executable
118 """
119 dependencies_dumper_t( target, source, env ).dump()
120 return 0
121
122
123 def register_builder( env, name ):
124 """"shared library dependencies builder scanner"""
125 builder = env.Builder( action = SCons.Action.Action(build_it, "dumping dependencies '$TARGET'")
126 #target_factory - is a factory function that the Builder will use to turn
127 #any targets specified as strings into SCons Nodes
128 , target_factory=env.fs.File )
129 env.Append(BUILDERS={name: builder })
130
131 def exists(env):
132 """the "combine" builder always exists"""
133 return True
Usage example:
1 import dependencies_builder
2
3 env = Environment( ... )
4 dependencies_builder.register_builder( env, 'DumpSODependencies' )
5
6
7 library = env.SharedLibrary( ... )
8 installed_lib = env.Install( ..., library )
9 library_dependencies = installed_lib[0].abspath + '.txt'
10 dependencies = env.DumpSODependencies( library_dependencies, library )
11 env.SideEffect('#serialize_dependencies', dependencies)
12 env.Alias(targetname, library_dependencies)
13 env.AlwaysBuild( library_dependencies )
