mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-03 18:47:53 +00:00
8c10235da0
Currently, when there is both an expandlibs descriptor and an actual static library, expandlibs picks the static library. This has the side effect that if there are object files in the static library that aren't directly used, they're dropped when linking, even when they export symbols that would be exported in the final linked binary. In most cases in the code base, files are not dropped that way. The most notable counter-example is xpcomglue, where actually not dropping files leads to link failure because of missing symbols those files reference (yes, that would tend to say the glue is broken in some way). On the opposite side, there is mozglue, which does have both a descriptor and a static library (the latter being necessary for the SDK), and that linking as a static library drops files that shouldn't be dropped (like jemalloc). We're currently relying on -Wl,--whole-archive for those files not to be dropped, but that won't really be possible without much hassle in a world where mozglue dependencies live in moz.build land. Switching expandlibs to use descriptors when they exist, even when there is a static library (so, the opposite of the current behavior) allows to drop -Wl,--whole-archive and prepare for a better future. However, as mentioned, xpcomglue does still require to be linked through the static library, so we need to make it a static library only. To achieve that, we make NO_EXPAND_LIBS now actually mean no expandlibs and use that to build the various different xpcomglues.
432 lines
19 KiB
Python
432 lines
19 KiB
Python
import subprocess
|
|
import unittest
|
|
import sys
|
|
import os
|
|
import imp
|
|
from tempfile import mkdtemp
|
|
from shutil import rmtree
|
|
import mozunit
|
|
|
|
from UserString import UserString
|
|
# Create a controlled configuration for use by expandlibs
|
|
config_win = {
|
|
'AR': 'lib',
|
|
'AR_EXTRACT': '',
|
|
'DLL_PREFIX': '',
|
|
'LIB_PREFIX': '',
|
|
'OBJ_SUFFIX': '.obj',
|
|
'LIB_SUFFIX': '.lib',
|
|
'DLL_SUFFIX': '.dll',
|
|
'IMPORT_LIB_SUFFIX': '.lib',
|
|
'LIBS_DESC_SUFFIX': '.desc',
|
|
'EXPAND_LIBS_LIST_STYLE': 'list',
|
|
}
|
|
config_unix = {
|
|
'AR': 'ar',
|
|
'AR_EXTRACT': 'ar -x',
|
|
'DLL_PREFIX': 'lib',
|
|
'LIB_PREFIX': 'lib',
|
|
'OBJ_SUFFIX': '.o',
|
|
'LIB_SUFFIX': '.a',
|
|
'DLL_SUFFIX': '.so',
|
|
'IMPORT_LIB_SUFFIX': '',
|
|
'LIBS_DESC_SUFFIX': '.desc',
|
|
'EXPAND_LIBS_LIST_STYLE': 'linkerscript',
|
|
}
|
|
|
|
config = sys.modules['expandlibs_config'] = imp.new_module('expandlibs_config')
|
|
|
|
from expandlibs import LibDescriptor, ExpandArgs, relativize
|
|
from expandlibs_gen import generate
|
|
from expandlibs_exec import ExpandArgsMore, SectionFinder
|
|
|
|
def Lib(name):
|
|
return config.LIB_PREFIX + name + config.LIB_SUFFIX
|
|
|
|
def Obj(name):
|
|
return name + config.OBJ_SUFFIX
|
|
|
|
def Dll(name):
|
|
return config.DLL_PREFIX + name + config.DLL_SUFFIX
|
|
|
|
def ImportLib(name):
|
|
if not len(config.IMPORT_LIB_SUFFIX): return Dll(name)
|
|
return config.LIB_PREFIX + name + config.IMPORT_LIB_SUFFIX
|
|
|
|
class TestRelativize(unittest.TestCase):
|
|
def test_relativize(self):
|
|
'''Test relativize()'''
|
|
os_path_exists = os.path.exists
|
|
def exists(path):
|
|
return True
|
|
os.path.exists = exists
|
|
self.assertEqual(relativize(os.path.abspath(os.curdir)), os.curdir)
|
|
self.assertEqual(relativize(os.path.abspath(os.pardir)), os.pardir)
|
|
self.assertEqual(relativize(os.path.join(os.curdir, 'a')), 'a')
|
|
self.assertEqual(relativize(os.path.join(os.path.abspath(os.curdir), 'a')), 'a')
|
|
# relativize is expected to return the absolute path if it is shorter
|
|
self.assertEqual(relativize(os.sep), os.sep)
|
|
os.path.exists = os.path.exists
|
|
|
|
class TestLibDescriptor(unittest.TestCase):
|
|
def test_serialize(self):
|
|
'''Test LibDescriptor's serialization'''
|
|
desc = LibDescriptor()
|
|
desc[LibDescriptor.KEYS[0]] = ['a', 'b']
|
|
self.assertEqual(str(desc), "{0} = a b".format(LibDescriptor.KEYS[0]))
|
|
desc['unsupported-key'] = ['a']
|
|
self.assertEqual(str(desc), "{0} = a b".format(LibDescriptor.KEYS[0]))
|
|
desc[LibDescriptor.KEYS[1]] = ['c', 'd', 'e']
|
|
self.assertEqual(str(desc),
|
|
"{0} = a b\n{1} = c d e"
|
|
.format(LibDescriptor.KEYS[0], LibDescriptor.KEYS[1]))
|
|
desc[LibDescriptor.KEYS[0]] = []
|
|
self.assertEqual(str(desc), "{0} = c d e".format(LibDescriptor.KEYS[1]))
|
|
|
|
def test_read(self):
|
|
'''Test LibDescriptor's initialization'''
|
|
desc_list = ["# Comment",
|
|
"{0} = a b".format(LibDescriptor.KEYS[1]),
|
|
"", # Empty line
|
|
"foo = bar", # Should be discarded
|
|
"{0} = c d e".format(LibDescriptor.KEYS[0])]
|
|
desc = LibDescriptor(desc_list)
|
|
self.assertEqual(desc[LibDescriptor.KEYS[1]], ['a', 'b'])
|
|
self.assertEqual(desc[LibDescriptor.KEYS[0]], ['c', 'd', 'e'])
|
|
self.assertEqual(False, 'foo' in desc)
|
|
|
|
def wrap_method(conf, wrapped_method):
|
|
'''Wrapper used to call a test with a specific configuration'''
|
|
def _method(self):
|
|
for key in conf:
|
|
setattr(config, key, conf[key])
|
|
self.init()
|
|
try:
|
|
wrapped_method(self)
|
|
except:
|
|
raise
|
|
finally:
|
|
self.cleanup()
|
|
return _method
|
|
|
|
class ReplicateTests(type):
|
|
'''Replicates tests for unix and windows variants'''
|
|
def __new__(cls, clsName, bases, dict):
|
|
for name in [key for key in dict if key.startswith('test_')]:
|
|
dict[name + '_unix'] = wrap_method(config_unix, dict[name])
|
|
dict[name + '_unix'].__doc__ = dict[name].__doc__ + ' (unix)'
|
|
dict[name + '_win'] = wrap_method(config_win, dict[name])
|
|
dict[name + '_win'].__doc__ = dict[name].__doc__ + ' (win)'
|
|
del dict[name]
|
|
return type.__new__(cls, clsName, bases, dict)
|
|
|
|
class TestCaseWithTmpDir(unittest.TestCase):
|
|
__metaclass__ = ReplicateTests
|
|
def init(self):
|
|
self.tmpdir = os.path.abspath(mkdtemp(dir=os.curdir))
|
|
|
|
def cleanup(self):
|
|
rmtree(self.tmpdir)
|
|
|
|
def touch(self, files):
|
|
for f in files:
|
|
open(f, 'w').close()
|
|
|
|
def tmpfile(self, *args):
|
|
return os.path.join(self.tmpdir, *args)
|
|
|
|
class TestExpandLibsGen(TestCaseWithTmpDir):
|
|
def test_generate(self):
|
|
'''Test library descriptor generation'''
|
|
files = [self.tmpfile(f) for f in
|
|
[Lib('a'), Obj('b'), Lib('c'), Obj('d'), Obj('e'), Lib('f')]]
|
|
self.touch(files[:-1])
|
|
self.touch([files[-1] + config.LIBS_DESC_SUFFIX])
|
|
|
|
desc = generate(files)
|
|
self.assertEqual(desc['OBJS'], [self.tmpfile(Obj(s)) for s in ['b', 'd', 'e']])
|
|
self.assertEqual(desc['LIBS'], [self.tmpfile(Lib(s)) for s in ['a', 'c', 'f']])
|
|
|
|
self.assertRaises(Exception, generate, files + [self.tmpfile(Obj('z'))])
|
|
self.assertRaises(Exception, generate, files + [self.tmpfile(Lib('y'))])
|
|
|
|
class TestExpandInit(TestCaseWithTmpDir):
|
|
def init(self):
|
|
''' Initializes test environment for library expansion tests'''
|
|
super(TestExpandInit, self).init()
|
|
# Create 2 fake libraries, each containing 3 objects, and the second
|
|
# including the first one and another library.
|
|
os.mkdir(self.tmpfile('libx'))
|
|
os.mkdir(self.tmpfile('liby'))
|
|
self.libx_files = [self.tmpfile('libx', Obj(f)) for f in ['g', 'h', 'i']]
|
|
self.liby_files = [self.tmpfile('liby', Obj(f)) for f in ['j', 'k', 'l']] + [self.tmpfile('liby', Lib('z'))]
|
|
self.touch(self.libx_files + self.liby_files)
|
|
with open(self.tmpfile('libx', Lib('x') + config.LIBS_DESC_SUFFIX), 'w') as f:
|
|
f.write(str(generate(self.libx_files)))
|
|
with open(self.tmpfile('liby', Lib('y') + config.LIBS_DESC_SUFFIX), 'w') as f:
|
|
f.write(str(generate(self.liby_files + [self.tmpfile('libx', Lib('x'))])))
|
|
|
|
# Create various objects and libraries
|
|
self.arg_files = [self.tmpfile(f) for f in [Lib('a'), Obj('b'), Obj('c'), Lib('d'), Obj('e')]]
|
|
# We always give library names (LIB_PREFIX/SUFFIX), even for
|
|
# dynamic/import libraries
|
|
self.files = self.arg_files + [self.tmpfile(ImportLib('f'))]
|
|
self.arg_files += [self.tmpfile(Lib('f'))]
|
|
self.touch(self.files)
|
|
|
|
def assertRelEqual(self, args1, args2):
|
|
self.assertEqual(args1, [relativize(a) for a in args2])
|
|
|
|
class TestExpandArgs(TestExpandInit):
|
|
def test_expand(self):
|
|
'''Test library expansion'''
|
|
# Expanding arguments means libraries with a descriptor are expanded
|
|
# with the descriptor content, and import libraries are used when
|
|
# a library doesn't exist
|
|
args = ExpandArgs(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))])
|
|
self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files)
|
|
|
|
# When a library exists at the same time as a descriptor, we still use
|
|
# the descriptor.
|
|
self.touch([self.tmpfile('libx', Lib('x'))])
|
|
args = ExpandArgs(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))])
|
|
self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files)
|
|
|
|
self.touch([self.tmpfile('liby', Lib('y'))])
|
|
args = ExpandArgs(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))])
|
|
self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files)
|
|
|
|
class TestExpandArgsMore(TestExpandInit):
|
|
def test_makelist(self):
|
|
'''Test grouping object files in lists'''
|
|
# ExpandArgsMore does the same as ExpandArgs
|
|
with ExpandArgsMore(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))]) as args:
|
|
self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files)
|
|
|
|
# But also has an extra method replacing object files with a list
|
|
args.makelist()
|
|
# self.files has objects at #1, #2, #4
|
|
self.assertRelEqual(args[:3], ['foo', '-bar'] + self.files[:1])
|
|
self.assertRelEqual(args[4:], [self.files[3]] + self.files[5:] + [self.tmpfile('liby', Lib('z'))])
|
|
|
|
# Check the list file content
|
|
objs = [f for f in self.files + self.liby_files + self.libx_files if f.endswith(config.OBJ_SUFFIX)]
|
|
if config.EXPAND_LIBS_LIST_STYLE == "linkerscript":
|
|
self.assertNotEqual(args[3][0], '@')
|
|
filename = args[3]
|
|
content = ['INPUT("{0}")'.format(relativize(f)) for f in objs]
|
|
with open(filename, 'r') as f:
|
|
self.assertEqual([l.strip() for l in f.readlines() if len(l.strip())], content)
|
|
elif config.EXPAND_LIBS_LIST_STYLE == "list":
|
|
self.assertEqual(args[3][0], '@')
|
|
filename = args[3][1:]
|
|
content = objs
|
|
with open(filename, 'r') as f:
|
|
self.assertRelEqual([l.strip() for l in f.readlines() if len(l.strip())], content)
|
|
|
|
tmp = args.tmp
|
|
# Check that all temporary files are properly removed
|
|
self.assertEqual(True, all([not os.path.exists(f) for f in tmp]))
|
|
|
|
def test_extract(self):
|
|
'''Test library extraction'''
|
|
# Divert subprocess.call
|
|
subprocess_call = subprocess.call
|
|
subprocess_check_output = subprocess.check_output
|
|
def call(args, **kargs):
|
|
if config.AR == 'lib':
|
|
self.assertEqual(args[:2], [config.AR, '-NOLOGO'])
|
|
self.assertTrue(args[2].startswith('-EXTRACT:'))
|
|
extract = [args[2][len('-EXTRACT:'):]]
|
|
self.assertTrue(extract)
|
|
args = args[3:]
|
|
else:
|
|
# The command called is always AR_EXTRACT
|
|
ar_extract = config.AR_EXTRACT.split()
|
|
self.assertEqual(args[:len(ar_extract)], ar_extract)
|
|
args = args[len(ar_extract):]
|
|
# Remaining argument is always one library
|
|
self.assertEqual(len(args), 1)
|
|
arg = args[0]
|
|
self.assertEqual(os.path.splitext(arg)[1], config.LIB_SUFFIX)
|
|
# Simulate file extraction
|
|
lib = os.path.splitext(os.path.basename(arg))[0]
|
|
if config.AR != 'lib':
|
|
extract = [lib, lib + '2']
|
|
extract = [os.path.join(kargs['cwd'], f) for f in extract]
|
|
if config.AR != 'lib':
|
|
extract = [Obj(f) for f in extract]
|
|
if not lib in extracted:
|
|
extracted[lib] = []
|
|
extracted[lib].extend(extract)
|
|
self.touch(extract)
|
|
subprocess.call = call
|
|
|
|
def check_output(args, **kargs):
|
|
# The command called is always AR
|
|
ar = config.AR
|
|
self.assertEqual(args[0:3], [ar, '-NOLOGO', '-LIST'])
|
|
# Remaining argument is always one library
|
|
self.assertRelEqual([os.path.splitext(arg)[1] for arg in args[3:]],
|
|
[config.LIB_SUFFIX])
|
|
# Simulate LIB -NOLOGO -LIST
|
|
lib = os.path.splitext(os.path.basename(args[3]))[0]
|
|
return '%s\n%s\n' % (Obj(lib), Obj(lib + '2'))
|
|
subprocess.check_output = check_output
|
|
|
|
# ExpandArgsMore does the same as ExpandArgs
|
|
self.touch([self.tmpfile('liby', Lib('y'))])
|
|
for iteration in (1, 2):
|
|
with ExpandArgsMore(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))]) as args:
|
|
files = self.files + self.liby_files + self.libx_files
|
|
|
|
self.assertRelEqual(args, ['foo', '-bar'] + files)
|
|
|
|
extracted = {}
|
|
# ExpandArgsMore also has an extra method extracting static libraries
|
|
# when possible
|
|
args.extract()
|
|
|
|
# With AR_EXTRACT, it uses the descriptors when there are, and
|
|
# actually
|
|
# extracts the remaining libraries
|
|
extracted_args = []
|
|
for f in files:
|
|
if f.endswith(config.LIB_SUFFIX):
|
|
base = os.path.splitext(os.path.basename(f))[0]
|
|
# On the first iteration, we test the behavior of
|
|
# extracting archives that don't have a copy of their
|
|
# contents next to them, which is to use the file
|
|
# extracted from the archive in a temporary directory.
|
|
# On the second iteration, we test extracting archives
|
|
# that do have a copy of their contents next to them,
|
|
# in which case those contents are used instead of the
|
|
# temporarily extracted files.
|
|
if iteration == 1:
|
|
extracted_args.extend(sorted(extracted[base]))
|
|
else:
|
|
dirname = os.path.dirname(f[len(self.tmpdir)+1:])
|
|
if base.endswith('f'):
|
|
dirname = os.path.join(dirname, 'foo', 'bar')
|
|
extracted_args.extend([self.tmpfile(dirname, Obj(base)), self.tmpfile(dirname, Obj(base + '2'))])
|
|
else:
|
|
extracted_args.append(f)
|
|
self.assertRelEqual(args, ['foo', '-bar'] + extracted_args)
|
|
|
|
tmp = args.tmp
|
|
# Check that all temporary files are properly removed
|
|
self.assertEqual(True, all([not os.path.exists(f) for f in tmp]))
|
|
|
|
# Create archives contents next to them for the second iteration.
|
|
base = os.path.splitext(Lib('_'))[0]
|
|
self.touch(self.tmpfile(Obj(base.replace('_', suffix))) for suffix in ('a', 'a2', 'd', 'd2'))
|
|
try:
|
|
os.makedirs(self.tmpfile('foo', 'bar'))
|
|
except:
|
|
pass
|
|
self.touch(self.tmpfile('foo', 'bar', Obj(base.replace('_', suffix))) for suffix in ('f', 'f2'))
|
|
self.touch(self.tmpfile('liby', Obj(base.replace('_', suffix))) for suffix in ('z', 'z2'))
|
|
|
|
# Restore subprocess.call and subprocess.check_output
|
|
subprocess.call = subprocess_call
|
|
subprocess.check_output = subprocess_check_output
|
|
|
|
class FakeProcess(object):
|
|
def __init__(self, out, err = ''):
|
|
self.out = out
|
|
self.err = err
|
|
|
|
def communicate(self):
|
|
return (self.out, self.err)
|
|
|
|
OBJDUMPS = {
|
|
'foo.o': '''
|
|
00000000 g F .text\t00000001 foo
|
|
00000000 g F .text._Z6foobarv\t00000001 _Z6foobarv
|
|
00000000 g F .text.hello\t00000001 hello
|
|
00000000 g F .text._ZThn4_6foobarv\t00000001 _ZThn4_6foobarv
|
|
''',
|
|
'bar.o': '''
|
|
00000000 g F .text.hi\t00000001 hi
|
|
00000000 g F .text.hot._Z6barbazv\t00000001 .hidden _Z6barbazv
|
|
''',
|
|
}
|
|
|
|
PRINT_ICF = '''
|
|
ld: ICF folding section '.text.hello' in file 'foo.o'into '.text.hi' in file 'bar.o'
|
|
ld: ICF folding section '.foo' in file 'foo.o'into '.foo' in file 'bar.o'
|
|
'''
|
|
|
|
class SubprocessPopen(object):
|
|
def __init__(self, test):
|
|
self.test = test
|
|
|
|
def __call__(self, args, stdout = None, stderr = None):
|
|
self.test.assertEqual(stdout, subprocess.PIPE)
|
|
self.test.assertEqual(stderr, subprocess.PIPE)
|
|
if args[0] == 'objdump':
|
|
self.test.assertEqual(args[1], '-t')
|
|
self.test.assertTrue(args[2] in OBJDUMPS)
|
|
return FakeProcess(OBJDUMPS[args[2]])
|
|
else:
|
|
return FakeProcess('', PRINT_ICF)
|
|
|
|
class TestSectionFinder(unittest.TestCase):
|
|
def test_getSections(self):
|
|
'''Test SectionFinder'''
|
|
# Divert subprocess.Popen
|
|
subprocess_popen = subprocess.Popen
|
|
subprocess.Popen = SubprocessPopen(self)
|
|
config.EXPAND_LIBS_ORDER_STYLE = 'linkerscript'
|
|
config.OBJ_SUFFIX = '.o'
|
|
config.LIB_SUFFIX = '.a'
|
|
finder = SectionFinder(['foo.o', 'bar.o'])
|
|
self.assertEqual(finder.getSections('foobar'), [])
|
|
self.assertEqual(finder.getSections('_Z6barbazv'), ['.text.hot._Z6barbazv'])
|
|
self.assertEqual(finder.getSections('_Z6foobarv'), ['.text._Z6foobarv', '.text._ZThn4_6foobarv'])
|
|
self.assertEqual(finder.getSections('_ZThn4_6foobarv'), ['.text._Z6foobarv', '.text._ZThn4_6foobarv'])
|
|
subprocess.Popen = subprocess_popen
|
|
|
|
class TestSymbolOrder(unittest.TestCase):
|
|
def test_getOrderedSections(self):
|
|
'''Test ExpandMoreArgs' _getOrderedSections'''
|
|
# Divert subprocess.Popen
|
|
subprocess_popen = subprocess.Popen
|
|
subprocess.Popen = SubprocessPopen(self)
|
|
config.EXPAND_LIBS_ORDER_STYLE = 'linkerscript'
|
|
config.OBJ_SUFFIX = '.o'
|
|
config.LIB_SUFFIX = '.a'
|
|
config.LD_PRINT_ICF_SECTIONS = ''
|
|
args = ExpandArgsMore(['foo', '-bar', 'bar.o', 'foo.o'])
|
|
self.assertEqual(args._getOrderedSections(['_Z6foobarv', '_Z6barbazv']), ['.text._Z6foobarv', '.text._ZThn4_6foobarv', '.text.hot._Z6barbazv'])
|
|
self.assertEqual(args._getOrderedSections(['_ZThn4_6foobarv', '_Z6barbazv']), ['.text._Z6foobarv', '.text._ZThn4_6foobarv', '.text.hot._Z6barbazv'])
|
|
subprocess.Popen = subprocess_popen
|
|
|
|
def test_getFoldedSections(self):
|
|
'''Test ExpandMoreArgs' _getFoldedSections'''
|
|
# Divert subprocess.Popen
|
|
subprocess_popen = subprocess.Popen
|
|
subprocess.Popen = SubprocessPopen(self)
|
|
config.LD_PRINT_ICF_SECTIONS = '-Wl,--print-icf-sections'
|
|
args = ExpandArgsMore(['foo', '-bar', 'bar.o', 'foo.o'])
|
|
self.assertEqual(args._getFoldedSections(), {'.text.hello': ['.text.hi'], '.text.hi': ['.text.hello']})
|
|
subprocess.Popen = subprocess_popen
|
|
|
|
def test_getOrderedSectionsWithICF(self):
|
|
'''Test ExpandMoreArgs' _getOrderedSections, with ICF'''
|
|
# Divert subprocess.Popen
|
|
subprocess_popen = subprocess.Popen
|
|
subprocess.Popen = SubprocessPopen(self)
|
|
config.EXPAND_LIBS_ORDER_STYLE = 'linkerscript'
|
|
config.OBJ_SUFFIX = '.o'
|
|
config.LIB_SUFFIX = '.a'
|
|
config.LD_PRINT_ICF_SECTIONS = '-Wl,--print-icf-sections'
|
|
args = ExpandArgsMore(['foo', '-bar', 'bar.o', 'foo.o'])
|
|
self.assertEqual(args._getOrderedSections(['hello', '_Z6barbazv']), ['.text.hello', '.text.hi', '.text.hot._Z6barbazv'])
|
|
self.assertEqual(args._getOrderedSections(['_ZThn4_6foobarv', 'hi', '_Z6barbazv']), ['.text._Z6foobarv', '.text._ZThn4_6foobarv', '.text.hi', '.text.hello', '.text.hot._Z6barbazv'])
|
|
subprocess.Popen = subprocess_popen
|
|
|
|
|
|
if __name__ == '__main__':
|
|
mozunit.main()
|